opencandle 0.5.0 → 0.7.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 (574) hide show
  1. package/README.md +170 -186
  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 +66 -7
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +13 -3
  11. package/dist/config.js +25 -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/cache.d.ts +8 -11
  17. package/dist/infra/cache.js +17 -15
  18. package/dist/infra/cache.js.map +1 -1
  19. package/dist/infra/http-client.d.ts +4 -1
  20. package/dist/infra/http-client.js +59 -6
  21. package/dist/infra/http-client.js.map +1 -1
  22. package/dist/infra/index.d.ts +2 -3
  23. package/dist/infra/index.js +2 -3
  24. package/dist/infra/index.js.map +1 -1
  25. package/dist/infra/native-dependencies.js +2 -2
  26. package/dist/infra/native-dependencies.js.map +1 -1
  27. package/dist/infra/node-version.js.map +1 -1
  28. package/dist/infra/opencandle-paths.d.ts +0 -3
  29. package/dist/infra/opencandle-paths.js +4 -11
  30. package/dist/infra/opencandle-paths.js.map +1 -1
  31. package/dist/infra/rate-limiter.js +12 -9
  32. package/dist/infra/rate-limiter.js.map +1 -1
  33. package/dist/market-state/alert-conditions.d.ts +34 -0
  34. package/dist/market-state/alert-conditions.js +23 -0
  35. package/dist/market-state/alert-conditions.js.map +1 -0
  36. package/dist/market-state/alert-runner.d.ts +55 -0
  37. package/dist/market-state/alert-runner.js +634 -0
  38. package/dist/market-state/alert-runner.js.map +1 -0
  39. package/dist/market-state/daily-report.d.ts +26 -0
  40. package/dist/market-state/daily-report.js +179 -0
  41. package/dist/market-state/daily-report.js.map +1 -0
  42. package/dist/market-state/local-automation-service.d.ts +25 -0
  43. package/dist/market-state/local-automation-service.js +119 -0
  44. package/dist/market-state/local-automation-service.js.map +1 -0
  45. package/dist/market-state/notification-delivery.d.ts +14 -0
  46. package/dist/market-state/notification-delivery.js +139 -0
  47. package/dist/market-state/notification-delivery.js.map +1 -0
  48. package/dist/market-state/resolve-for-mutation.d.ts +10 -0
  49. package/dist/market-state/resolve-for-mutation.js +15 -0
  50. package/dist/market-state/resolve-for-mutation.js.map +1 -0
  51. package/dist/market-state/resolve.d.ts +14 -0
  52. package/dist/market-state/resolve.js +89 -0
  53. package/dist/market-state/resolve.js.map +1 -0
  54. package/dist/market-state/service.d.ts +527 -0
  55. package/dist/market-state/service.js +1099 -0
  56. package/dist/market-state/service.js.map +1 -0
  57. package/dist/memory/index.d.ts +7 -7
  58. package/dist/memory/index.js +6 -6
  59. package/dist/memory/index.js.map +1 -1
  60. package/dist/memory/manager.js +11 -11
  61. package/dist/memory/manager.js.map +1 -1
  62. package/dist/memory/retrieval.js +7 -4
  63. package/dist/memory/retrieval.js.map +1 -1
  64. package/dist/memory/sqlite.js +385 -3
  65. package/dist/memory/sqlite.js.map +1 -1
  66. package/dist/memory/storage.js +1 -2
  67. package/dist/memory/storage.js.map +1 -1
  68. package/dist/memory/tool-defaults.js +64 -28
  69. package/dist/memory/tool-defaults.js.map +1 -1
  70. package/dist/memory/types.js.map +1 -1
  71. package/dist/monitor.d.ts +2 -0
  72. package/dist/monitor.js +104 -0
  73. package/dist/monitor.js.map +1 -0
  74. package/dist/onboarding/connect.d.ts +2 -2
  75. package/dist/onboarding/connect.js +13 -8
  76. package/dist/onboarding/connect.js.map +1 -1
  77. package/dist/onboarding/credential-interceptor.js +1 -1
  78. package/dist/onboarding/credential-interceptor.js.map +1 -1
  79. package/dist/onboarding/degradation-accumulator.js +1 -3
  80. package/dist/onboarding/degradation-accumulator.js.map +1 -1
  81. package/dist/onboarding/provider-status.d.ts +48 -0
  82. package/dist/onboarding/provider-status.js +285 -0
  83. package/dist/onboarding/provider-status.js.map +1 -0
  84. package/dist/onboarding/providers.d.ts +85 -8
  85. package/dist/onboarding/providers.js +83 -18
  86. package/dist/onboarding/providers.js.map +1 -1
  87. package/dist/onboarding/state.d.ts +1 -0
  88. package/dist/onboarding/state.js +5 -0
  89. package/dist/onboarding/state.js.map +1 -1
  90. package/dist/onboarding/tool-helpers.js +1 -1
  91. package/dist/onboarding/tool-helpers.js.map +1 -1
  92. package/dist/onboarding/tool-tags.d.ts +12 -1
  93. package/dist/onboarding/tool-tags.js +37 -5
  94. package/dist/onboarding/tool-tags.js.map +1 -1
  95. package/dist/onboarding/validation.d.ts +2 -2
  96. package/dist/onboarding/validation.js +1 -1
  97. package/dist/onboarding/validation.js.map +1 -1
  98. package/dist/pi/opencandle-extension.d.ts +8 -0
  99. package/dist/pi/opencandle-extension.js +502 -42
  100. package/dist/pi/opencandle-extension.js.map +1 -1
  101. package/dist/pi/session.d.ts +1 -1
  102. package/dist/pi/session.js +3 -1
  103. package/dist/pi/session.js.map +1 -1
  104. package/dist/pi/setup.js +8 -3
  105. package/dist/pi/setup.js.map +1 -1
  106. package/dist/pi/tool-adapter.d.ts +4 -1
  107. package/dist/pi/tool-adapter.js +10 -6
  108. package/dist/pi/tool-adapter.js.map +1 -1
  109. package/dist/prompts/context-builder.d.ts +1 -1
  110. package/dist/prompts/context-builder.js +20 -7
  111. package/dist/prompts/context-builder.js.map +1 -1
  112. package/dist/prompts/policy-cards.d.ts +1 -1
  113. package/dist/prompts/policy-cards.js +2 -2
  114. package/dist/prompts/policy-cards.js.map +1 -1
  115. package/dist/prompts/sections.d.ts +1 -1
  116. package/dist/prompts/symbol-preflight.d.ts +20 -0
  117. package/dist/prompts/symbol-preflight.js +49 -0
  118. package/dist/prompts/symbol-preflight.js.map +1 -0
  119. package/dist/prompts/workflow-prompts.d.ts +1 -1
  120. package/dist/prompts/workflow-prompts.js +54 -16
  121. package/dist/prompts/workflow-prompts.js.map +1 -1
  122. package/dist/providers/alpha-vantage.d.ts +1 -1
  123. package/dist/providers/alpha-vantage.js +26 -7
  124. package/dist/providers/alpha-vantage.js.map +1 -1
  125. package/dist/providers/coingecko.js +1 -1
  126. package/dist/providers/coingecko.js.map +1 -1
  127. package/dist/providers/errors.d.ts +5 -0
  128. package/dist/providers/errors.js +11 -0
  129. package/dist/providers/errors.js.map +1 -0
  130. package/dist/providers/exa-search.d.ts +2 -2
  131. package/dist/providers/exa-search.js +19 -11
  132. package/dist/providers/exa-search.js.map +1 -1
  133. package/dist/providers/external-tool-error.d.ts +10 -0
  134. package/dist/providers/external-tool-error.js +21 -0
  135. package/dist/providers/external-tool-error.js.map +1 -0
  136. package/dist/providers/fear-greed.js +1 -1
  137. package/dist/providers/fear-greed.js.map +1 -1
  138. package/dist/providers/finnhub.js +3 -5
  139. package/dist/providers/finnhub.js.map +1 -1
  140. package/dist/providers/fred.js +2 -2
  141. package/dist/providers/fred.js.map +1 -1
  142. package/dist/providers/index.d.ts +7 -6
  143. package/dist/providers/index.js +6 -5
  144. package/dist/providers/index.js.map +1 -1
  145. package/dist/providers/reddit-cli.d.ts +36 -0
  146. package/dist/providers/reddit-cli.js +201 -0
  147. package/dist/providers/reddit-cli.js.map +1 -0
  148. package/dist/providers/reddit.d.ts +1 -1
  149. package/dist/providers/reddit.js +9 -37
  150. package/dist/providers/reddit.js.map +1 -1
  151. package/dist/providers/sec-edgar.d.ts +1 -0
  152. package/dist/providers/sec-edgar.js +12 -4
  153. package/dist/providers/sec-edgar.js.map +1 -1
  154. package/dist/providers/tradingview.d.ts +47 -0
  155. package/dist/providers/tradingview.js +275 -0
  156. package/dist/providers/tradingview.js.map +1 -0
  157. package/dist/providers/twitter-cli.d.ts +40 -0
  158. package/dist/providers/twitter-cli.js +153 -0
  159. package/dist/providers/twitter-cli.js.map +1 -0
  160. package/dist/providers/twitter.d.ts +0 -8
  161. package/dist/providers/twitter.js +8 -60
  162. package/dist/providers/twitter.js.map +1 -1
  163. package/dist/providers/web-search.js +26 -12
  164. package/dist/providers/web-search.js.map +1 -1
  165. package/dist/providers/with-fallback.js +4 -2
  166. package/dist/providers/with-fallback.js.map +1 -1
  167. package/dist/providers/wrap-provider.d.ts +2 -3
  168. package/dist/providers/wrap-provider.js +44 -8
  169. package/dist/providers/wrap-provider.js.map +1 -1
  170. package/dist/providers/yahoo-finance.d.ts +1 -1
  171. package/dist/providers/yahoo-finance.js +153 -48
  172. package/dist/providers/yahoo-finance.js.map +1 -1
  173. package/dist/routing/classify-intent.d.ts +6 -0
  174. package/dist/routing/classify-intent.js +78 -7
  175. package/dist/routing/classify-intent.js.map +1 -1
  176. package/dist/routing/defaults.d.ts +1 -1
  177. package/dist/routing/entity-extractor.d.ts +1 -0
  178. package/dist/routing/entity-extractor.js +234 -29
  179. package/dist/routing/entity-extractor.js.map +1 -1
  180. package/dist/routing/fund-symbols.d.ts +2 -0
  181. package/dist/routing/fund-symbols.js +55 -0
  182. package/dist/routing/fund-symbols.js.map +1 -0
  183. package/dist/routing/horizon.d.ts +1 -0
  184. package/dist/routing/horizon.js +10 -0
  185. package/dist/routing/horizon.js.map +1 -0
  186. package/dist/routing/index.d.ts +10 -10
  187. package/dist/routing/index.js +6 -6
  188. package/dist/routing/index.js.map +1 -1
  189. package/dist/routing/planning.d.ts +2 -2
  190. package/dist/routing/planning.js +65 -34
  191. package/dist/routing/planning.js.map +1 -1
  192. package/dist/routing/route-manifest.d.ts +2 -2
  193. package/dist/routing/route-manifest.js +25 -4
  194. package/dist/routing/route-manifest.js.map +1 -1
  195. package/dist/routing/router-llm-client.js.map +1 -1
  196. package/dist/routing/router-prompt.js +7 -9
  197. package/dist/routing/router-prompt.js.map +1 -1
  198. package/dist/routing/router-types.d.ts +1 -0
  199. package/dist/routing/router.js +137 -22
  200. package/dist/routing/router.js.map +1 -1
  201. package/dist/routing/slot-resolver.d.ts +1 -1
  202. package/dist/routing/slot-resolver.js +2 -4
  203. package/dist/routing/slot-resolver.js.map +1 -1
  204. package/dist/routing/symbol-disambiguator.d.ts +11 -0
  205. package/dist/routing/symbol-disambiguator.js +52 -0
  206. package/dist/routing/symbol-disambiguator.js.map +1 -0
  207. package/dist/routing/turn-context.d.ts +1 -1
  208. package/dist/routing/turn-context.js +1 -1
  209. package/dist/routing/turn-context.js.map +1 -1
  210. package/dist/routing/types.d.ts +2 -0
  211. package/dist/runtime/answer-contracts.d.ts +1 -1
  212. package/dist/runtime/answer-contracts.js +48 -9
  213. package/dist/runtime/answer-contracts.js.map +1 -1
  214. package/dist/runtime/artifact-contracts.js.map +1 -1
  215. package/dist/runtime/planning-evidence.js +47 -26
  216. package/dist/runtime/planning-evidence.js.map +1 -1
  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 +13 -5
  224. package/dist/runtime/session-coordinator.js +160 -20
  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 +7 -5
  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 +10 -11
  243. package/dist/sentiment/index.js +10 -20
  244. package/dist/sentiment/index.js.map +1 -1
  245. package/dist/sentiment/insights.d.ts +17 -0
  246. package/dist/sentiment/insights.js +206 -0
  247. package/dist/sentiment/insights.js.map +1 -0
  248. package/dist/sentiment/keywords.js +26 -4
  249. package/dist/sentiment/keywords.js.map +1 -1
  250. package/dist/sentiment/pipeline.d.ts +2 -2
  251. package/dist/sentiment/pipeline.js +14 -2
  252. package/dist/sentiment/pipeline.js.map +1 -1
  253. package/dist/sentiment/scorer.d.ts +2 -0
  254. package/dist/sentiment/scorer.js +11 -2
  255. package/dist/sentiment/scorer.js.map +1 -1
  256. package/dist/sentiment/store.d.ts +1 -1
  257. package/dist/sentiment/store.js +1 -1
  258. package/dist/sentiment/store.js.map +1 -1
  259. package/dist/sentiment/trends.d.ts +1 -1
  260. package/dist/sentiment/trends.js.map +1 -1
  261. package/dist/sentiment/types.d.ts +2 -0
  262. package/dist/sentiment/types.js.map +1 -1
  263. package/dist/system-prompt.js +6 -9
  264. package/dist/system-prompt.js.map +1 -1
  265. package/dist/tool-kit.d.ts +7 -7
  266. package/dist/tool-kit.js +4 -4
  267. package/dist/tool-kit.js.map +1 -1
  268. package/dist/tools/fundamentals/company-overview.js +11 -6
  269. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  270. package/dist/tools/fundamentals/comps.js +18 -9
  271. package/dist/tools/fundamentals/comps.js.map +1 -1
  272. package/dist/tools/fundamentals/dcf.js +23 -11
  273. package/dist/tools/fundamentals/dcf.js.map +1 -1
  274. package/dist/tools/fundamentals/earnings.js +8 -3
  275. package/dist/tools/fundamentals/earnings.js.map +1 -1
  276. package/dist/tools/fundamentals/financials.js +8 -3
  277. package/dist/tools/fundamentals/financials.js.map +1 -1
  278. package/dist/tools/fundamentals/sec-filings.js +21 -6
  279. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  280. package/dist/tools/index.d.ts +27 -20
  281. package/dist/tools/index.js +55 -43
  282. package/dist/tools/index.js.map +1 -1
  283. package/dist/tools/interaction/ask-user.js +15 -3
  284. package/dist/tools/interaction/ask-user.js.map +1 -1
  285. package/dist/tools/macro/fear-greed.js.map +1 -1
  286. package/dist/tools/macro/fred-data.d.ts +1 -1
  287. package/dist/tools/macro/fred-data.js +17 -6
  288. package/dist/tools/macro/fred-data.js.map +1 -1
  289. package/dist/tools/market/crypto-history.js +3 -1
  290. package/dist/tools/market/crypto-history.js.map +1 -1
  291. package/dist/tools/market/crypto-price.js +3 -1
  292. package/dist/tools/market/crypto-price.js.map +1 -1
  293. package/dist/tools/market/screen-stocks.d.ts +18 -0
  294. package/dist/tools/market/screen-stocks.js +252 -0
  295. package/dist/tools/market/screen-stocks.js.map +1 -0
  296. package/dist/tools/market/search-ticker.js +160 -8
  297. package/dist/tools/market/search-ticker.js.map +1 -1
  298. package/dist/tools/market/stock-history.d.ts +2 -2
  299. package/dist/tools/market/stock-history.js +26 -7
  300. package/dist/tools/market/stock-history.js.map +1 -1
  301. package/dist/tools/market/stock-quote.js +5 -3
  302. package/dist/tools/market/stock-quote.js.map +1 -1
  303. package/dist/tools/options/greeks.js +1 -1
  304. package/dist/tools/options/greeks.js.map +1 -1
  305. package/dist/tools/options/option-chain.js +19 -6
  306. package/dist/tools/options/option-chain.js.map +1 -1
  307. package/dist/tools/portfolio/alerts.d.ts +15 -0
  308. package/dist/tools/portfolio/alerts.js +357 -0
  309. package/dist/tools/portfolio/alerts.js.map +1 -0
  310. package/dist/tools/portfolio/correlation.d.ts +1 -1
  311. package/dist/tools/portfolio/correlation.js +33 -13
  312. package/dist/tools/portfolio/correlation.js.map +1 -1
  313. package/dist/tools/portfolio/daily-report.d.ts +8 -0
  314. package/dist/tools/portfolio/daily-report.js +83 -0
  315. package/dist/tools/portfolio/daily-report.js.map +1 -0
  316. package/dist/tools/portfolio/holdings-overlap.js +10 -3
  317. package/dist/tools/portfolio/holdings-overlap.js.map +1 -1
  318. package/dist/tools/portfolio/notifications.d.ts +7 -0
  319. package/dist/tools/portfolio/notifications.js +43 -0
  320. package/dist/tools/portfolio/notifications.js.map +1 -0
  321. package/dist/tools/portfolio/predictions.d.ts +12 -6
  322. package/dist/tools/portfolio/predictions.js +337 -87
  323. package/dist/tools/portfolio/predictions.js.map +1 -1
  324. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  325. package/dist/tools/portfolio/risk-analysis.js +45 -6
  326. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  327. package/dist/tools/portfolio/tracker.d.ts +4 -3
  328. package/dist/tools/portfolio/tracker.js +246 -101
  329. package/dist/tools/portfolio/tracker.js.map +1 -1
  330. package/dist/tools/portfolio/watchlist.d.ts +6 -4
  331. package/dist/tools/portfolio/watchlist.js +208 -108
  332. package/dist/tools/portfolio/watchlist.js.map +1 -1
  333. package/dist/tools/sentiment/insight-format.d.ts +2 -0
  334. package/dist/tools/sentiment/insight-format.js +36 -0
  335. package/dist/tools/sentiment/insight-format.js.map +1 -0
  336. package/dist/tools/sentiment/query-match.d.ts +3 -0
  337. package/dist/tools/sentiment/query-match.js +113 -0
  338. package/dist/tools/sentiment/query-match.js.map +1 -0
  339. package/dist/tools/sentiment/reddit-sentiment.d.ts +12 -1
  340. package/dist/tools/sentiment/reddit-sentiment.js +266 -107
  341. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  342. package/dist/tools/sentiment/sentiment-summary.d.ts +9 -1
  343. package/dist/tools/sentiment/sentiment-summary.js +223 -205
  344. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  345. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  346. package/dist/tools/sentiment/sentiment-trend.js +12 -2
  347. package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
  348. package/dist/tools/sentiment/twitter-sentiment.d.ts +11 -1
  349. package/dist/tools/sentiment/twitter-sentiment.js +188 -58
  350. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  351. package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
  352. package/dist/tools/sentiment/untrusted-text.js +17 -0
  353. package/dist/tools/sentiment/untrusted-text.js.map +1 -0
  354. package/dist/tools/sentiment/web-search.js +9 -13
  355. package/dist/tools/sentiment/web-search.js.map +1 -1
  356. package/dist/tools/sentiment/web-sentiment.js +19 -3
  357. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  358. package/dist/tools/technical/backtest.d.ts +1 -1
  359. package/dist/tools/technical/backtest.js +27 -20
  360. package/dist/tools/technical/backtest.js.map +1 -1
  361. package/dist/tools/technical/indicators.js +23 -5
  362. package/dist/tools/technical/indicators.js.map +1 -1
  363. package/dist/types/index.d.ts +3 -3
  364. package/dist/types/index.js.map +1 -1
  365. package/dist/types/market.d.ts +1 -0
  366. package/dist/types/portfolio.d.ts +14 -4
  367. package/dist/types/sentiment.d.ts +52 -0
  368. package/dist/workflows/compare-assets.d.ts +0 -3
  369. package/dist/workflows/compare-assets.js +20 -11
  370. package/dist/workflows/compare-assets.js.map +1 -1
  371. package/dist/workflows/index.d.ts +3 -4
  372. package/dist/workflows/index.js +3 -3
  373. package/dist/workflows/index.js.map +1 -1
  374. package/dist/workflows/options-screener.d.ts +0 -3
  375. package/dist/workflows/options-screener.js +4 -11
  376. package/dist/workflows/options-screener.js.map +1 -1
  377. package/dist/workflows/portfolio-builder.d.ts +0 -3
  378. package/dist/workflows/portfolio-builder.js +0 -8
  379. package/dist/workflows/portfolio-builder.js.map +1 -1
  380. package/gui/server/ask-user-bridge.ts +1 -1
  381. package/gui/server/automation-heartbeat.ts +97 -0
  382. package/gui/server/background-quotes.ts +97 -1
  383. package/gui/server/chat-event-adapter.ts +32 -10
  384. package/gui/server/chat-run-session.ts +16 -0
  385. package/gui/server/invoke-tool.ts +160 -3
  386. package/gui/server/live-chat-event-adapter.ts +21 -6
  387. package/gui/server/market-state-api.ts +315 -0
  388. package/gui/server/model-setup.ts +156 -2
  389. package/gui/server/private-api-access.ts +62 -0
  390. package/gui/server/projector.ts +18 -9
  391. package/gui/server/prompt-observation.ts +4 -7
  392. package/gui/server/quote-snapshot-store.ts +50 -0
  393. package/gui/server/server.ts +218 -451
  394. package/gui/server/session-actions.ts +186 -1
  395. package/gui/server/shutdown.ts +47 -0
  396. package/gui/server/tool-invoke-ack.ts +49 -0
  397. package/gui/server/tool-metadata.ts +101 -24
  398. package/gui/server/websocket.ts +13 -3
  399. package/gui/server/writer-lock.ts +6 -2
  400. package/gui/server/ws-hub.ts +311 -0
  401. package/gui/shared/chat-events.ts +16 -1
  402. package/gui/shared/event-reducer.ts +24 -6
  403. package/gui/web/dist/assets/CatalogOverlay-CgeY5Pkp.js +1 -0
  404. package/gui/web/dist/assets/index-C6W_2eAn.js +69 -0
  405. package/gui/web/dist/assets/index-hwbx24a5.css +1 -0
  406. package/gui/web/dist/index.html +2 -2
  407. package/package.json +9 -6
  408. package/src/analysts/contracts.ts +10 -23
  409. package/src/analysts/orchestrator.ts +8 -43
  410. package/src/cli.ts +76 -12
  411. package/src/config.ts +44 -9
  412. package/src/index.ts +1 -1
  413. package/src/infra/cache.ts +41 -30
  414. package/src/infra/http-client.ts +72 -6
  415. package/src/infra/index.ts +6 -10
  416. package/src/infra/native-dependencies.ts +8 -3
  417. package/src/infra/node-version.ts +3 -1
  418. package/src/infra/opencandle-paths.ts +3 -14
  419. package/src/infra/rate-limiter.ts +22 -19
  420. package/src/market-state/alert-conditions.ts +82 -0
  421. package/src/market-state/alert-runner.ts +863 -0
  422. package/src/market-state/daily-report.ts +247 -0
  423. package/src/market-state/local-automation-service.ts +162 -0
  424. package/src/market-state/notification-delivery.ts +158 -0
  425. package/src/market-state/resolve-for-mutation.ts +24 -0
  426. package/src/market-state/resolve.ts +112 -0
  427. package/src/market-state/service.ts +2344 -0
  428. package/src/memory/index.ts +7 -7
  429. package/src/memory/manager.ts +14 -16
  430. package/src/memory/retrieval.ts +8 -7
  431. package/src/memory/sqlite.ts +407 -6
  432. package/src/memory/storage.ts +5 -15
  433. package/src/memory/tool-defaults.ts +60 -39
  434. package/src/memory/types.ts +3 -3
  435. package/src/monitor.ts +121 -0
  436. package/src/onboarding/connect.ts +24 -31
  437. package/src/onboarding/credential-interceptor.ts +3 -15
  438. package/src/onboarding/degradation-accumulator.ts +1 -3
  439. package/src/onboarding/provider-status.ts +410 -0
  440. package/src/onboarding/providers.ts +144 -45
  441. package/src/onboarding/state.ts +13 -15
  442. package/src/onboarding/tool-helpers.ts +2 -9
  443. package/src/onboarding/tool-tags.ts +51 -8
  444. package/src/onboarding/validation.ts +16 -22
  445. package/src/pi/opencandle-extension.ts +643 -101
  446. package/src/pi/session.ts +7 -5
  447. package/src/pi/setup.ts +61 -43
  448. package/src/pi/tool-adapter.ts +19 -6
  449. package/src/prompts/context-builder.ts +24 -13
  450. package/src/prompts/policy-cards.ts +3 -3
  451. package/src/prompts/sections.ts +1 -1
  452. package/src/prompts/symbol-preflight.ts +80 -0
  453. package/src/prompts/workflow-prompts.ts +77 -28
  454. package/src/providers/alpha-vantage.ts +58 -39
  455. package/src/providers/coingecko.ts +2 -5
  456. package/src/providers/errors.ts +9 -0
  457. package/src/providers/exa-search.ts +24 -22
  458. package/src/providers/external-tool-error.ts +20 -0
  459. package/src/providers/fear-greed.ts +1 -1
  460. package/src/providers/finnhub.ts +7 -6
  461. package/src/providers/fred.ts +3 -3
  462. package/src/providers/index.ts +14 -6
  463. package/src/providers/reddit-cli.ts +317 -0
  464. package/src/providers/reddit.ts +14 -59
  465. package/src/providers/sec-edgar.ts +20 -6
  466. package/src/providers/tradingview.ts +399 -0
  467. package/src/providers/twitter-cli.ts +233 -0
  468. package/src/providers/twitter.ts +8 -79
  469. package/src/providers/web-search.ts +30 -20
  470. package/src/providers/with-fallback.ts +8 -7
  471. package/src/providers/wrap-provider.ts +49 -10
  472. package/src/providers/yahoo-finance.ts +204 -66
  473. package/src/routing/classify-intent.ts +101 -10
  474. package/src/routing/defaults.ts +1 -1
  475. package/src/routing/entity-extractor.ts +287 -38
  476. package/src/routing/fund-symbols.ts +58 -0
  477. package/src/routing/horizon.ts +7 -0
  478. package/src/routing/index.ts +48 -48
  479. package/src/routing/planning.ts +145 -53
  480. package/src/routing/route-manifest.ts +37 -15
  481. package/src/routing/router-llm-client.ts +4 -4
  482. package/src/routing/router-prompt.ts +15 -19
  483. package/src/routing/router-types.ts +2 -5
  484. package/src/routing/router.ts +251 -53
  485. package/src/routing/slot-resolver.ts +34 -11
  486. package/src/routing/symbol-disambiguator.ts +72 -0
  487. package/src/routing/turn-context.ts +6 -9
  488. package/src/routing/types.ts +2 -0
  489. package/src/runtime/answer-contracts.ts +105 -45
  490. package/src/runtime/artifact-contracts.ts +2 -1
  491. package/src/runtime/planning-evidence.ts +157 -66
  492. package/src/runtime/prompt-step.ts +1 -16
  493. package/src/runtime/run-context.ts +12 -2
  494. package/src/runtime/session-coordinator.ts +238 -63
  495. package/src/runtime/session-title.ts +60 -0
  496. package/src/runtime/tool-defaults-wrapper.ts +13 -5
  497. package/src/runtime/validation.ts +1 -4
  498. package/src/runtime/workflow-events.ts +7 -7
  499. package/src/runtime/workflow-runner.ts +5 -11
  500. package/src/sentiment/adapters/finnhub.ts +7 -2
  501. package/src/sentiment/adapters/reddit.ts +2 -2
  502. package/src/sentiment/adapters/twitter.ts +1 -1
  503. package/src/sentiment/adapters/web.ts +1 -1
  504. package/src/sentiment/index.ts +17 -26
  505. package/src/sentiment/insights.ts +269 -0
  506. package/src/sentiment/keywords.ts +26 -4
  507. package/src/sentiment/pipeline.ts +28 -5
  508. package/src/sentiment/scorer.ts +13 -2
  509. package/src/sentiment/store.ts +2 -2
  510. package/src/sentiment/trends.ts +9 -3
  511. package/src/sentiment/types.ts +8 -4
  512. package/src/system-prompt.ts +6 -9
  513. package/src/tool-kit.ts +10 -9
  514. package/src/tools/fundamentals/company-overview.ts +19 -9
  515. package/src/tools/fundamentals/comps.ts +68 -55
  516. package/src/tools/fundamentals/dcf.ts +145 -95
  517. package/src/tools/fundamentals/earnings.ts +16 -6
  518. package/src/tools/fundamentals/financials.ts +16 -7
  519. package/src/tools/fundamentals/sec-filings.ts +37 -16
  520. package/src/tools/index.ts +56 -43
  521. package/src/tools/interaction/ask-user.ts +22 -10
  522. package/src/tools/macro/fear-greed.ts +1 -1
  523. package/src/tools/macro/fred-data.ts +58 -46
  524. package/src/tools/market/crypto-history.ts +8 -3
  525. package/src/tools/market/crypto-price.ts +6 -6
  526. package/src/tools/market/screen-stocks.ts +279 -0
  527. package/src/tools/market/search-ticker.ts +218 -17
  528. package/src/tools/market/stock-history.ts +37 -12
  529. package/src/tools/market/stock-quote.ts +10 -7
  530. package/src/tools/options/greeks.ts +5 -5
  531. package/src/tools/options/option-chain.ts +41 -17
  532. package/src/tools/portfolio/alerts.ts +457 -0
  533. package/src/tools/portfolio/correlation.ts +47 -20
  534. package/src/tools/portfolio/daily-report.ts +101 -0
  535. package/src/tools/portfolio/holdings-overlap.ts +31 -15
  536. package/src/tools/portfolio/notifications.ts +45 -0
  537. package/src/tools/portfolio/predictions.ts +406 -106
  538. package/src/tools/portfolio/risk-analysis.ts +46 -7
  539. package/src/tools/portfolio/tracker.ts +270 -109
  540. package/src/tools/portfolio/watchlist.ts +250 -121
  541. package/src/tools/sentiment/insight-format.ts +50 -0
  542. package/src/tools/sentiment/query-match.ts +117 -0
  543. package/src/tools/sentiment/reddit-sentiment.ts +360 -121
  544. package/src/tools/sentiment/sentiment-summary.ts +302 -235
  545. package/src/tools/sentiment/sentiment-trend.ts +24 -7
  546. package/src/tools/sentiment/twitter-sentiment.ts +264 -73
  547. package/src/tools/sentiment/untrusted-text.ts +21 -0
  548. package/src/tools/sentiment/web-search.ts +21 -18
  549. package/src/tools/sentiment/web-sentiment.ts +30 -10
  550. package/src/tools/technical/backtest.ts +32 -22
  551. package/src/tools/technical/indicators.ts +39 -14
  552. package/src/types/index.ts +8 -3
  553. package/src/types/market.ts +1 -0
  554. package/src/types/portfolio.ts +14 -4
  555. package/src/types/sentiment.ts +61 -2
  556. package/src/workflows/compare-assets.ts +33 -21
  557. package/src/workflows/index.ts +3 -4
  558. package/src/workflows/options-screener.ts +27 -29
  559. package/src/workflows/portfolio-builder.ts +34 -27
  560. package/dist/infra/browser.d.ts +0 -35
  561. package/dist/infra/browser.js +0 -103
  562. package/dist/infra/browser.js.map +0 -1
  563. package/dist/tools/interaction/twitter-login.d.ts +0 -8
  564. package/dist/tools/interaction/twitter-login.js +0 -77
  565. package/dist/tools/interaction/twitter-login.js.map +0 -1
  566. package/dist/workflows/types.d.ts +0 -4
  567. package/dist/workflows/types.js +0 -2
  568. package/dist/workflows/types.js.map +0 -1
  569. package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +0 -1
  570. package/gui/web/dist/assets/index-Bxt9QpLX.css +0 -1
  571. package/gui/web/dist/assets/index-CZ9DHZYy.js +0 -67
  572. package/src/infra/browser.ts +0 -111
  573. package/src/tools/interaction/twitter-login.ts +0 -93
  574. package/src/workflows/types.ts +0 -4
@@ -1,14 +1,33 @@
1
- import { httpGet } from "../infra/http-client.js";
2
- import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
1
+ import YahooFinance from "yahoo-finance2";
2
+ import type { OptionsResult as YahooFinance2OptionsResult } from "yahoo-finance2/modules/options";
3
+ import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
4
+ import { HttpError, httpGet } from "../infra/http-client.js";
3
5
  import { rateLimiter } from "../infra/rate-limiter.js";
4
- import { StealthBrowser } from "../infra/browser.js";
5
- import type { StockQuote, OHLCV } from "../types/market.js";
6
- import type { OptionsChain, OptionContract, OptionsMarketSession, OptionsQuoteStatus } from "../types/options.js";
7
- import type { FundHoldings } from "../types/portfolio.js";
8
6
  import { computeGreeks } from "../tools/options/greeks.js";
7
+ import type { OHLCV, StockQuote } from "../types/market.js";
8
+ import type {
9
+ OptionContract,
10
+ OptionsChain,
11
+ OptionsMarketSession,
12
+ OptionsQuoteStatus,
13
+ } from "../types/options.js";
14
+ import type { FundHoldings } from "../types/portfolio.js";
15
+ import { InvalidSymbolError } from "./errors.js";
9
16
 
10
17
  const BASE_URL = "https://query1.finance.yahoo.com/v8/finance/chart";
11
18
  const QUOTE_SUMMARY_URL = "https://query1.finance.yahoo.com/v10/finance/quoteSummary";
19
+ const STALE_QUOTE_MAX_RETRY_AFTER_MS = 1_000;
20
+
21
+ let yahooFinance2Client: InstanceType<typeof YahooFinance> | undefined;
22
+
23
+ function getYahooFinance2Client(): InstanceType<typeof YahooFinance> {
24
+ yahooFinance2Client ??= new YahooFinance({
25
+ suppressNotices: ["yahooSurvey", "ripHistorical"],
26
+ });
27
+ return yahooFinance2Client;
28
+ }
29
+
30
+ type YahooNumber = number | { raw?: number; fmt?: string };
12
31
 
13
32
  interface YahooChartResponse {
14
33
  chart: {
@@ -42,10 +61,10 @@ interface YahooQuoteSummaryResponse {
42
61
  holdings?: Array<{
43
62
  symbol?: string;
44
63
  holdingName?: string;
45
- holdingPercent?: number;
64
+ holdingPercent?: YahooNumber;
46
65
  }>;
47
66
  equityHoldings?: {
48
- sectorWeightings?: Array<Record<string, number>>;
67
+ sectorWeightings?: Array<Record<string, YahooNumber>>;
49
68
  };
50
69
  };
51
70
  }>;
@@ -64,6 +83,7 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
64
83
  const url = `${BASE_URL}/${encodeURIComponent(symbol)}?interval=1d&range=1d`;
65
84
  const data = await httpGet<YahooChartResponse>(url, {
66
85
  headers: { "User-Agent": "OpenCandle/1.0" },
86
+ maxRetryAfterMs: STALE_QUOTE_MAX_RETRY_AFTER_MS,
67
87
  });
68
88
 
69
89
  if (data.chart.error) {
@@ -97,8 +117,16 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
97
117
  week52High: meta.fiftyTwoWeekHigh ?? 0,
98
118
  week52Low: meta.fiftyTwoWeekLow ?? 0,
99
119
  timestamp: Date.now(),
120
+ currency:
121
+ typeof meta.currency === "string" && meta.currency.trim() !== ""
122
+ ? meta.currency.trim().toUpperCase()
123
+ : null,
100
124
  };
101
125
 
126
+ if (isZeroResultQuote(quote)) {
127
+ throw new InvalidSymbolError(symbol.toUpperCase(), "yahoo");
128
+ }
129
+
102
130
  cache.set(cacheKey, quote, TTL.QUOTE);
103
131
  return quote;
104
132
  } catch (error) {
@@ -108,6 +136,16 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
108
136
  }
109
137
  }
110
138
 
139
+ function isZeroResultQuote(quote: StockQuote): boolean {
140
+ return (
141
+ quote.price === 0 &&
142
+ quote.volume === 0 &&
143
+ quote.week52High === 0 &&
144
+ quote.week52Low === 0 &&
145
+ quote.marketCap === 0
146
+ );
147
+ }
148
+
111
149
  export async function getHistory(
112
150
  symbol: string,
113
151
  range: string = "6mo",
@@ -162,14 +200,12 @@ export async function getFundHoldings(symbol: string): Promise<FundHoldings> {
162
200
  try {
163
201
  await rateLimiter.acquire("yahoo");
164
202
 
165
- const modules = encodeURIComponent("price,topHoldings");
166
- const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(normalizedSymbol)}?modules=${modules}`;
167
- const data = await httpGet<YahooQuoteSummaryResponse>(url, {
168
- headers: { "User-Agent": "OpenCandle/1.0" },
169
- });
203
+ const data = await getFundHoldingsSummary(normalizedSymbol);
170
204
  const result = data.quoteSummary.result?.[0];
171
205
  if (data.quoteSummary.error) {
172
- throw new Error(`Yahoo Finance: ${data.quoteSummary.error.description ?? data.quoteSummary.error.code ?? "quoteSummary error"}`);
206
+ throw new Error(
207
+ `Yahoo Finance: ${data.quoteSummary.error.description ?? data.quoteSummary.error.code ?? "quoteSummary error"}`,
208
+ );
173
209
  }
174
210
  if (!result?.topHoldings?.holdings?.length) {
175
211
  throw new Error(`Yahoo Finance: no fund holdings returned for ${normalizedSymbol}`);
@@ -183,11 +219,13 @@ export async function getFundHoldings(symbol: string): Promise<FundHoldings> {
183
219
  const holdingSymbol = holding.symbol?.trim().toUpperCase();
184
220
  const weight = normalizeHoldingWeight(holding.holdingPercent);
185
221
  if (!holdingSymbol || weight === undefined) return [];
186
- return [{
187
- symbol: holdingSymbol,
188
- name: holding.holdingName?.trim() || holdingSymbol,
189
- weight,
190
- }];
222
+ return [
223
+ {
224
+ symbol: holdingSymbol,
225
+ name: holding.holdingName?.trim() || holdingSymbol,
226
+ weight,
227
+ },
228
+ ];
191
229
  }),
192
230
  sectorWeights: normalizeSectorWeights(result.topHoldings.equityHoldings?.sectorWeightings),
193
231
  };
@@ -204,13 +242,56 @@ export async function getFundHoldings(symbol: string): Promise<FundHoldings> {
204
242
  }
205
243
  }
206
244
 
207
- function normalizeHoldingWeight(value: number | undefined): number | undefined {
208
- if (value === undefined || !Number.isFinite(value) || value <= 0) return undefined;
209
- return value > 1 ? roundWeight(value / 100) : roundWeight(value);
245
+ async function getFundHoldingsSummary(symbol: string): Promise<YahooQuoteSummaryResponse> {
246
+ try {
247
+ return await fetchFundHoldingsSummary(symbol);
248
+ } catch (error) {
249
+ if (!isYahooAuthError(error)) throw error;
250
+ return fetchFundHoldingsSummaryWithCrumb(symbol);
251
+ }
252
+ }
253
+
254
+ async function fetchFundHoldingsSummary(symbol: string): Promise<YahooQuoteSummaryResponse> {
255
+ const modules = encodeURIComponent("price,topHoldings");
256
+ const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}`;
257
+ return httpGet<YahooQuoteSummaryResponse>(url, {
258
+ headers: { "User-Agent": "OpenCandle/1.0" },
259
+ });
260
+ }
261
+
262
+ async function fetchFundHoldingsSummaryWithCrumb(
263
+ symbol: string,
264
+ ): Promise<YahooQuoteSummaryResponse> {
265
+ const modules = encodeURIComponent("price,topHoldings");
266
+ const { crumb, cookie } = await getYahooCrumb();
267
+ const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}&crumb=${encodeURIComponent(crumb)}`;
268
+ try {
269
+ return await httpGet<YahooQuoteSummaryResponse>(url, {
270
+ headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
271
+ });
272
+ } catch (error) {
273
+ if (!isYahooAuthError(error)) throw error;
274
+ clearCrumbCache();
275
+ const fresh = await getYahooCrumb();
276
+ const retryUrl = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}&crumb=${encodeURIComponent(fresh.crumb)}`;
277
+ return httpGet<YahooQuoteSummaryResponse>(retryUrl, {
278
+ headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
279
+ });
280
+ }
281
+ }
282
+
283
+ function isYahooAuthError(error: unknown): boolean {
284
+ return error instanceof HttpError && (error.status === 401 || error.status === 429);
285
+ }
286
+
287
+ function normalizeHoldingWeight(value: YahooNumber | undefined): number | undefined {
288
+ const numeric = typeof value === "number" ? value : value?.raw;
289
+ if (numeric === undefined || !Number.isFinite(numeric) || numeric <= 0) return undefined;
290
+ return numeric > 1 ? roundWeight(numeric / 100) : roundWeight(numeric);
210
291
  }
211
292
 
212
293
  function normalizeSectorWeights(
213
- sectors: Array<Record<string, number>> | undefined,
294
+ sectors: Array<Record<string, YahooNumber>> | undefined,
214
295
  ): Record<string, number> | undefined {
215
296
  if (!sectors?.length) return undefined;
216
297
  const weights: Record<string, number> = {};
@@ -231,6 +312,7 @@ function roundWeight(value: number): number {
231
312
 
232
313
  const BROWSER_UA =
233
314
  "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";
315
+ const YAHOO_RAW_FETCH_TIMEOUT_MS = 10_000;
234
316
 
235
317
  let cachedCrumb: { crumb: string; cookie: string; expiresAt: number } | null = null;
236
318
 
@@ -246,15 +328,30 @@ export async function getYahooCrumb(): Promise<{ crumb: string; cookie: string }
246
328
  // Step 1: Hit fc.yahoo.com to get a session cookie
247
329
  const cookieRes = await fetch("https://fc.yahoo.com/t", {
248
330
  headers: { "User-Agent": BROWSER_UA },
331
+ signal: yahooRawFetchSignal(),
249
332
  });
250
333
  const setCookie = cookieRes.headers.get("set-cookie") ?? "";
251
334
  const cookie = setCookie.split(";")[0]; // Extract just the cookie value
335
+ if (!cookie) {
336
+ if (!cookieRes.ok) {
337
+ throw new Error(
338
+ `Yahoo crumb cookie request failed: HTTP ${cookieRes.status} ${cookieRes.statusText}`.trim(),
339
+ );
340
+ }
341
+ throw new Error("Yahoo crumb cookie request did not return a session cookie");
342
+ }
252
343
 
253
344
  // Step 2: Use the cookie to get a crumb
254
345
  const crumbRes = await fetch("https://query2.finance.yahoo.com/v1/test/getcrumb", {
255
346
  headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
347
+ signal: yahooRawFetchSignal(),
256
348
  });
257
- const crumb = await crumbRes.text();
349
+ if (!crumbRes.ok) {
350
+ throw new Error(
351
+ `Yahoo crumb request failed: HTTP ${crumbRes.status} ${crumbRes.statusText}`.trim(),
352
+ );
353
+ }
354
+ const crumb = (await crumbRes.text()).trim();
258
355
 
259
356
  if (!crumb || crumb.includes("Unauthorized")) {
260
357
  throw new Error("Failed to acquire Yahoo Finance crumb");
@@ -281,25 +378,23 @@ interface YahooOptionsResponse {
281
378
  };
282
379
  }
283
380
 
284
- export async function getOptionsChain(
285
- symbol: string,
286
- expiration?: number,
287
- ): Promise<OptionsChain> {
381
+ export async function getOptionsChain(symbol: string, expiration?: number): Promise<OptionsChain> {
288
382
  const cacheKey = `yahoo:options:${symbol}:${expiration ?? "nearest"}`;
289
383
  const cached = cache.get<OptionsChain>(cacheKey);
290
384
  if (cached) return cached;
291
385
 
292
386
  await rateLimiter.acquire("yahoo");
293
387
 
294
- const { crumb, cookie } = await getYahooCrumb();
295
388
  const dateParam = expiration ? `&date=${expiration}` : "";
296
- const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
297
389
 
298
390
  let res: Response | null = null;
299
391
  let fetchError: unknown;
300
392
  try {
393
+ const { crumb, cookie } = await getYahooCrumb();
394
+ const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
301
395
  res = await fetch(url, {
302
396
  headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
397
+ signal: yahooRawFetchSignal(),
303
398
  });
304
399
  } catch (error) {
305
400
  fetchError = error;
@@ -313,6 +408,7 @@ export async function getOptionsChain(
313
408
  const retryUrl = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(fresh.crumb)}${dateParam}`;
314
409
  res = await fetch(retryUrl, {
315
410
  headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
411
+ signal: yahooRawFetchSignal(),
316
412
  });
317
413
  } catch (error) {
318
414
  fetchError = error;
@@ -324,9 +420,9 @@ export async function getOptionsChain(
324
420
  if (!res?.ok) {
325
421
  let browserError: unknown;
326
422
  try {
327
- const browserData = await fetchOptionsViaBrowser(symbol, expiration);
328
- if (browserData) {
329
- const chain = parseOptionsResponse(browserData);
423
+ const fallbackData = await fetchOptionsViaYahooFinance2(symbol, expiration);
424
+ if (fallbackData) {
425
+ const chain = parseOptionsResponse(fallbackData);
330
426
  cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
331
427
  return chain;
332
428
  }
@@ -339,15 +435,18 @@ export async function getOptionsChain(
339
435
  if (res) {
340
436
  const message = `Yahoo Finance options: HTTP ${res.status}`;
341
437
  if (browserError instanceof Error) {
342
- throw new Error(`${message}; browser fallback failed: ${browserError.message}`);
438
+ throw new Error(`${message}; yahoo-finance2 fallback failed: ${browserError.message}`);
343
439
  }
344
440
  throw new Error(message);
345
441
  }
346
442
  if (browserError instanceof Error) {
347
- const message = fetchError instanceof Error ? fetchError.message : "Yahoo Finance options: fetch failed";
348
- throw new Error(`${message}; browser fallback failed: ${browserError.message}`);
443
+ const message =
444
+ fetchError instanceof Error ? fetchError.message : "Yahoo Finance options: fetch failed";
445
+ throw new Error(`${message}; yahoo-finance2 fallback failed: ${browserError.message}`);
349
446
  }
350
- throw fetchError instanceof Error ? fetchError : new Error("Yahoo Finance options: fetch failed");
447
+ throw fetchError instanceof Error
448
+ ? fetchError
449
+ : new Error("Yahoo Finance options: fetch failed");
351
450
  }
352
451
 
353
452
  const data: YahooOptionsResponse = await res.json();
@@ -356,6 +455,10 @@ export async function getOptionsChain(
356
455
  return chain;
357
456
  }
358
457
 
458
+ function yahooRawFetchSignal(): AbortSignal {
459
+ return AbortSignal.timeout(YAHOO_RAW_FETCH_TIMEOUT_MS);
460
+ }
461
+
359
462
  /**
360
463
  * Compute time to expiry in years from a Yahoo expiration timestamp (midnight UTC).
361
464
  * US equity options expire at 4:00 PM ET. During EDT that is 20:00 UTC.
@@ -364,7 +467,7 @@ export async function getOptionsChain(
364
467
  */
365
468
  export function computeTimeToExpiry(expirationTs: number, nowMs: number = Date.now()): number {
366
469
  const MARKET_CLOSE_OFFSET_S = 21 * 3600; // 21:00 UTC ≈ 4 PM ET
367
- const MIN_TIME_YEARS = 1 / (365 * 24); // ~1 hour floor
470
+ const MIN_TIME_YEARS = 1 / (365 * 24); // ~1 hour floor
368
471
  const SECONDS_PER_YEAR = 365 * 24 * 3600;
369
472
 
370
473
  const expiryCloseTs = expirationTs + MARKET_CLOSE_OFFSET_S;
@@ -449,6 +552,9 @@ function parseOptionsResponse(data: YahooOptionsResponse): OptionsChain {
449
552
  const quote = result.quote;
450
553
  const underlyingPrice = quote.regularMarketPrice ?? 0;
451
554
  const opts = result.options[0];
555
+ if (!opts && underlyingPrice === 0) {
556
+ throw new InvalidSymbolError(result.underlyingSymbol, "yahoo");
557
+ }
452
558
  const riskFreeRate = 0.05;
453
559
 
454
560
  const expirationTs = opts.expirationDate;
@@ -458,7 +564,14 @@ function parseOptionsResponse(data: YahooOptionsResponse): OptionsChain {
458
564
  const mapContract = (c: any, type: "call" | "put"): OptionContract => {
459
565
  const strike = c.strike ?? c.strike?.raw ?? 0;
460
566
  const iv = c.impliedVolatility ?? c.impliedVolatility?.raw ?? 0;
461
- const greeks = computeGreeks({ type, spot: underlyingPrice, strike, timeYears, iv, riskFreeRate });
567
+ const greeks = computeGreeks({
568
+ type,
569
+ spot: underlyingPrice,
570
+ strike,
571
+ timeYears,
572
+ iv,
573
+ riskFreeRate,
574
+ });
462
575
  return {
463
576
  contractSymbol: c.contractSymbol ?? "",
464
577
  type,
@@ -485,7 +598,9 @@ function parseOptionsResponse(data: YahooOptionsResponse): OptionsChain {
485
598
  symbol: result.underlyingSymbol,
486
599
  underlyingPrice,
487
600
  expirationDate,
488
- expirationDates: result.expirationDates.map((ts) => new Date(ts * 1000).toISOString().split("T")[0]),
601
+ expirationDates: result.expirationDates.map(
602
+ (ts) => new Date(ts * 1000).toISOString().split("T")[0],
603
+ ),
489
604
  calls,
490
605
  puts,
491
606
  totalCallVolume,
@@ -497,38 +612,61 @@ function parseOptionsResponse(data: YahooOptionsResponse): OptionsChain {
497
612
  }
498
613
 
499
614
  /**
500
- * Fallback: fetch options data via Camoufox stealth browser.
501
- * Bypasses Yahoo's TLS fingerprinting and rate limiting.
615
+ * Fallback: fetch options data via yahoo-finance2 when the raw Yahoo path is blocked.
502
616
  */
503
- async function fetchOptionsViaBrowser(
617
+ async function fetchOptionsViaYahooFinance2(
504
618
  symbol: string,
505
619
  expiration?: number,
506
620
  ): Promise<YahooOptionsResponse | null> {
507
621
  try {
508
- // Avoid loading the script-heavy Yahoo Finance homepage: Playwright 1.60
509
- // can crash on some pageerror payloads emitted by finance.yahoo.com.
510
- // Navigating directly to Yahoo's JSON endpoints still uses the browser's
511
- // cookies/TLS fingerprint without requiring cross-origin fetch from page JS.
512
- const dateParam = expiration ? `&date=${expiration}` : "";
513
- return await StealthBrowser.run(async (page) => {
514
- await page.goto("https://query2.finance.yahoo.com/v1/test/getcrumb", {
515
- waitUntil: "domcontentloaded",
516
- timeout: 15000,
517
- });
518
- const crumb = (await page.locator("body").innerText()).trim();
519
- if (!crumb) return null;
520
-
521
- const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
522
- const response = await page.goto(url, {
523
- waitUntil: "domcontentloaded",
524
- timeout: 15000,
525
- });
526
- if (!response?.ok()) return null;
527
-
528
- const text = (await page.locator("body").innerText()).trim();
529
- return JSON.parse(text) as YahooOptionsResponse;
530
- });
622
+ const result = await getYahooFinance2Client().options(
623
+ symbol,
624
+ expiration ? { date: new Date(expiration * 1000) } : undefined,
625
+ );
626
+ return normalizeYahooFinance2OptionsResponse(result);
531
627
  } catch (error) {
532
628
  throw error instanceof Error ? error : new Error(String(error));
533
629
  }
534
630
  }
631
+
632
+ function normalizeYahooFinance2OptionsResponse(
633
+ data: YahooFinance2OptionsResult | YahooOptionsResponse,
634
+ ): YahooOptionsResponse {
635
+ if ("optionChain" in data) return data as YahooOptionsResponse;
636
+
637
+ const options = data.options.map((option) => ({
638
+ expirationDate: toYahooUnixSeconds(option.expirationDate),
639
+ calls: option.calls,
640
+ puts: option.puts,
641
+ }));
642
+ const strikes = [
643
+ ...new Set(
644
+ options
645
+ .flatMap((option) => [...option.calls, ...option.puts])
646
+ .map((contract) => Number((contract as { strike?: unknown }).strike))
647
+ .filter((strike) => Number.isFinite(strike)),
648
+ ),
649
+ ].sort((a, b) => a - b);
650
+
651
+ return {
652
+ optionChain: {
653
+ result: [
654
+ {
655
+ underlyingSymbol: data.underlyingSymbol,
656
+ expirationDates: data.expirationDates.map(toYahooUnixSeconds),
657
+ strikes,
658
+ quote: data.quote as Record<string, any>,
659
+ options,
660
+ },
661
+ ],
662
+ },
663
+ };
664
+ }
665
+
666
+ function toYahooUnixSeconds(value: Date | number | string): number {
667
+ if (value instanceof Date) return Math.floor(value.getTime() / 1000);
668
+ if (typeof value === "number") {
669
+ return value > 1_000_000_000_000 ? Math.floor(value / 1000) : value;
670
+ }
671
+ return Math.floor(new Date(value).getTime() / 1000);
672
+ }
@@ -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;
@@ -43,7 +43,9 @@ const RULES: Rule[] = [
43
43
  return (
44
44
  entities.symbols.length >= 1 &&
45
45
  (/\bi\s+own\b/.test(lower) || /\bmy\s+holdings\b/.test(lower)) &&
46
- (/\bportfolio\s+risk\b/.test(lower) || /\bbiggest\s+risk\b/.test(lower) || /\bconcentration\b/.test(lower))
46
+ (/\bportfolio\s+risk\b/.test(lower) ||
47
+ /\bbiggest\s+risk\b/.test(lower) ||
48
+ /\bconcentration\b/.test(lower))
47
49
  );
48
50
  },
49
51
  },
@@ -91,9 +93,7 @@ const RULES: Rule[] = [
91
93
  if (hasCompareKeywords && entities.symbols.length >= 2) return false;
92
94
 
93
95
  return (
94
- /\bbacktest\b/.test(lower) ||
95
- /\bsentiment\b/.test(lower) ||
96
- /\brate\s+cuts?\b/.test(lower)
96
+ /\bbacktest\b/.test(lower) || /\bsentiment\b/.test(lower) || /\brate\s+cuts?\b/.test(lower)
97
97
  );
98
98
  },
99
99
  },
@@ -145,6 +145,14 @@ const RULES: Rule[] = [
145
145
  return hasOptionKeywords && entities.symbols.length >= 1;
146
146
  },
147
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
+ },
148
156
  // Compare: keyword + 2+ symbols (uppercase)
149
157
  {
150
158
  workflow: "compare_assets",
@@ -165,8 +173,10 @@ const RULES: Rule[] = [
165
173
  confidence: 0.85,
166
174
  test: (input, entities) => {
167
175
  const lower = input.toLowerCase();
168
- return entities.symbols.length >= 2 &&
169
- /\bcompare\s+[a-z]{1,5}\b(?:\s*,?\s*(?:and\s+)?[a-z]{1,5}\b)+/.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
+ );
170
180
  },
171
181
  },
172
182
  // Compare: 2+ uppercase symbols without explicit keyword
@@ -275,11 +285,92 @@ export function classifyIntent(input: string): ClassificationResult {
275
285
  function isPortfolioEvaluationRequest(input: string): boolean {
276
286
  const lower = input.toLowerCase();
277
287
  const hasEvaluationIntent =
278
- /\b(?:evaluat(?:e|ion)|review|assess|analy[sz]e|prospects?|risks?|opportunities?|mitigat(?:e|ion)|adjustment)\b/.test(lower);
288
+ /\b(?:evaluat(?:e|ion)|review|assess|analy[sz]e|prospects?|risks?|opportunities?|mitigat(?:e|ion)|adjustment)\b/.test(
289
+ lower,
290
+ );
279
291
  const hasPortfolioObject =
280
- /\b(?:portfolio|allocation|asset\s+allocation|60\/40|equity|fixed\s+income|bonds?)\b/.test(lower);
292
+ /\b(?:portfolio|allocation|asset\s+allocation|60\/40|equity|fixed\s+income|bonds?)\b/.test(
293
+ lower,
294
+ );
281
295
  const hasConstructionIntent =
282
296
  /\b(?:build|create|construct|put\s+together|invest|allocate)\b/.test(lower) &&
283
- (/\$\s*\d|\b\d+(?:\.\d+)?\s*k\b|\bbudget\b|\bcapital\b/.test(lower));
297
+ /\$\s*\d|\b\d+(?:\.\d+)?\s*k\b|\bbudget\b|\bcapital\b/.test(lower);
284
298
  return hasEvaluationIntent && hasPortfolioObject && !hasConstructionIntent;
285
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
+ }
@@ -1,4 +1,4 @@
1
- import type { PortfolioSlots, OptionsScreenerSlots } from "./types.js";
1
+ import type { OptionsScreenerSlots, PortfolioSlots } from "./types.js";
2
2
 
3
3
  export const PORTFOLIO_DEFAULTS: Omit<PortfolioSlots, "budget"> = {
4
4
  riskProfile: "balanced",