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,14 @@
1
+ import { parseDteTarget } from "../routing/defaults.js";
2
+ import { areLikelyFundOrIndexSymbols, isFundOrIndexAssetScope } from "../routing/fund-symbols.js";
3
+ import { isLongInvestmentHorizon } from "../routing/horizon.js";
4
+ import type { RouterOutput } from "../routing/router-types.js";
1
5
  import type {
2
- PortfolioSlots,
3
- OptionsScreenerSlots,
4
6
  CompareAssetsSlots,
7
+ OptionsScreenerSlots,
8
+ PortfolioSlots,
5
9
  SlotResolution,
6
10
  SlotSource,
7
11
  } from "../routing/types.js";
8
- import type { RouterOutput } from "../routing/router-types.js";
9
- import { parseDteTarget } from "../routing/defaults.js";
10
12
 
11
13
  function tag(source: string | undefined): string {
12
14
  switch (source) {
@@ -14,7 +16,6 @@ function tag(source: string | undefined): string {
14
16
  return " [DEFAULT]";
15
17
  case "preference":
16
18
  return " [SAVED PREFERENCE]";
17
- case "user":
18
19
  default:
19
20
  return "";
20
21
  }
@@ -48,7 +49,11 @@ const DISPLAY_NAMES: Record<string, string> = {
48
49
  objective: "objective",
49
50
  moneynessPreference: "moneyness",
50
51
  liquidityMinimum: "liquidity",
52
+ optionStrategy: "option strategy",
53
+ costBasis: "cost basis",
54
+ shareQuantity: "share quantity",
51
55
  symbols: "symbols",
56
+ metrics: "metrics",
52
57
  };
53
58
 
54
59
  /**
@@ -62,6 +67,8 @@ export function buildDisclosureBlock(
62
67
  ): string {
63
68
  const userSpecified: string[] = [];
64
69
  const fromPreferences: string[] = [];
70
+ const fromPriorContext: string[] = [];
71
+ const fromMemory: string[] = [];
65
72
  const defaults: string[] = [];
66
73
 
67
74
  for (const [key, source] of Object.entries(slotSources)) {
@@ -75,6 +82,12 @@ export function buildDisclosureBlock(
75
82
  case "preference":
76
83
  fromPreferences.push(display);
77
84
  break;
85
+ case "prior_context":
86
+ fromPriorContext.push(display);
87
+ break;
88
+ case "memory":
89
+ fromMemory.push(display);
90
+ break;
78
91
  case "default":
79
92
  defaults.push(display);
80
93
  break;
@@ -84,7 +97,11 @@ export function buildDisclosureBlock(
84
97
  const lines: string[] = [];
85
98
  lines.push("Assumptions (reproduce this block exactly — do not relabel sources):");
86
99
  if (userSpecified.length > 0) lines.push(` User-specified: ${userSpecified.join(", ")}`);
87
- if (fromPreferences.length > 0) lines.push(` From saved preferences: ${fromPreferences.join(", ")}`);
100
+ if (fromPreferences.length > 0)
101
+ lines.push(` From saved preferences: ${fromPreferences.join(", ")}`);
102
+ if (fromPriorContext.length > 0)
103
+ lines.push(` From prior context: ${fromPriorContext.join(", ")}`);
104
+ if (fromMemory.length > 0) lines.push(` From memory: ${fromMemory.join(", ")}`);
88
105
  if (defaults.length > 0) lines.push(` Defaults: ${defaults.join(", ")}`);
89
106
  if (workflowConstraints && workflowConstraints.length > 0) {
90
107
  lines.push(` Workflow constraints: ${workflowConstraints.join(", ")}`);
@@ -124,7 +141,11 @@ function formatSlotValue(value: unknown): string {
124
141
 
125
142
  export function buildPortfolioPrompt(resolution: SlotResolution<PortfolioSlots>): string {
126
143
  const { resolved: s, sources } = resolution;
127
- const isEtfOnly = s.assetScope.toLowerCase().startsWith("etf");
144
+ const normalizedScope = s.assetScope.toLowerCase();
145
+ const isFundBuildingBlocks =
146
+ normalizedScope.includes("etf") ||
147
+ normalizedScope.includes("fund") ||
148
+ normalizedScope.includes("building_blocks");
128
149
 
129
150
  const disclosureBlock = buildDisclosureBlock(
130
151
  {
@@ -138,12 +159,14 @@ export function buildPortfolioPrompt(resolution: SlotResolution<PortfolioSlots>)
138
159
  sources as Record<string, SlotSource | undefined>,
139
160
  );
140
161
 
141
- const toolSteps = isEtfOnly
142
- ? `1. Identify ${s.positionCount} diverse ETF candidates appropriate for a ${s.riskProfile} ${s.timeHorizon} portfolio.
162
+ const toolSteps = isFundBuildingBlocks
163
+ ? `1. Identify ${s.positionCount} diversified fund/ETF building-block candidates appropriate for a ${s.riskProfile} ${s.timeHorizon} portfolio.
164
+ Include distinct asset-class roles such as core domestic equity, international equity, fixed income, short-duration or cash-like stability, and inflation-sensitive ballast when appropriate.
143
165
  2. Use get_stock_quote for each candidate to get current prices.
144
166
  3. Use analyze_risk on each candidate for volatility, Sharpe, and max drawdown.
145
167
  4. Use analyze_correlation across all candidates to check diversification.`
146
168
  : `1. Identify ${s.positionCount} diverse candidates appropriate for a ${s.riskProfile} ${s.timeHorizon} portfolio.
169
+ Avoid over-concentration in individual equities unless the user explicitly asked for stock picks; use diversified funds where they better fit the requested horizon and risk profile.
147
170
  2. Use get_stock_quote for each candidate to get current prices.
148
171
  3. Use get_company_overview for fundamentals on each candidate.
149
172
  4. Use analyze_risk on each candidate for volatility, Sharpe, and max drawdown.
@@ -162,17 +185,28 @@ Build a draft portfolio under these parameters:
162
185
  Steps:
163
186
  ${toolSteps}
164
187
 
188
+ Portfolio construction guardrails:
189
+ - For broad balanced portfolio requests, prefer diversified building blocks over individual-company concentration unless the user explicitly asks for stocks.
190
+ - For horizons under 5 years, include enough fixed-income, short-duration, cash-like, or inflation-sensitive ballast to make the drawdown risk match the horizon.
191
+ - If a candidate's risk metrics undermine its role (for example materially negative risk-adjusted returns, high drawdown, or excessive correlation), lower the allocation, name a role-equivalent replacement, or explain why you are keeping it.
192
+ - Keep rationale tied to each holding's role in this portfolio; do not paste company descriptions or generic issuer background.
193
+
165
194
  ${disclosureBlock}
166
195
 
167
196
  Response format:
168
197
  - Start with the assumptions block above exactly as written. Do not relabel source attribution anywhere else in your response.
198
+ - Then start the analysis with "Bottom line:" and directly say what portfolio you would build for the user.
169
199
  - Commit to the draft: give concrete percentages for each position, not ranges, and not "consider allocating X-Y%".
170
- - Present an allocation table: symbol, allocation %, dollar amount, and a one-line analyst rationale for each position (what the data showed).
200
+ - Present an allocation table: symbol, allocation %, dollar amount, current price used, estimated shares, role, and a one-line analyst rationale for each position (what the data showed and why it belongs in this portfolio).
201
+ - After the table, add a brief "Why this fits the horizon" summary explaining the growth/stability tradeoff and horizon-specific risks for the stated time horizon.
171
202
  - Include a risk summary (portfolio volatility, diversification quality) and an invalidation condition for the overall draft ("revisit if correlation exceeds 0.7 across the core ETFs" or equivalent).
203
+ - Include practical implementation notes: rebalance cadence, low-cost/liquid implementation, and tax/account caveats where relevant.
172
204
  - Suggest what to change for more growth or more safety.`;
173
205
  }
174
206
 
175
- export function buildOptionsScreenerPrompt(resolution: SlotResolution<OptionsScreenerSlots>): string {
207
+ export function buildOptionsScreenerPrompt(
208
+ resolution: SlotResolution<OptionsScreenerSlots>,
209
+ ): string {
176
210
  const { resolved: s, sources } = resolution;
177
211
 
178
212
  const dateStr = todayStr();
@@ -199,14 +233,15 @@ Ranking constraints:
199
233
  - Do NOT rank ultra-cheap near-zero-delta contracts as "best."
200
234
  `
201
235
  : "";
202
- const longDatedInstructions = s.dteTarget === "180_plus_days"
203
- ? `
236
+ const longDatedInstructions =
237
+ s.dteTarget === "180_plus_days"
238
+ ? `
204
239
  For LEAPS / long-dated options:
205
240
  - First call get_option_chain without an expiration to inspect available expirations.
206
241
  - Choose available expirations inside the target window, then call get_option_chain again with explicit \`expiration\` dates before ranking contracts.
207
242
  - Do not rank the nearest-expiration chain as a LEAPS result.
208
243
  `
209
- : "";
244
+ : "";
210
245
 
211
246
  const disclosureBlock = buildDisclosureBlock(
212
247
  {
@@ -216,11 +251,70 @@ For LEAPS / long-dated options:
216
251
  objective: s.objective,
217
252
  moneynessPreference: s.moneynessPreference,
218
253
  liquidityMinimum: s.liquidityMinimum,
254
+ ...(s.maxPremium !== undefined ? { maxPremium: formatBudget(s.maxPremium) } : {}),
255
+ ...(s.optionStrategy ? { optionStrategy: s.optionStrategy } : {}),
256
+ ...(s.costBasis !== undefined ? { costBasis: formatBudget(s.costBasis) } : {}),
257
+ ...(s.shareQuantity !== undefined ? { shareQuantity: `${s.shareQuantity} shares` } : {}),
258
+ ...(s.catalystSymbols?.length ? { catalystSymbols: s.catalystSymbols.join(", ") } : {}),
219
259
  },
220
260
  sources as Record<string, SlotSource | undefined>,
221
261
  workflowConstraints,
222
262
  );
223
263
 
264
+ const coveredCallContext = [
265
+ s.optionStrategy
266
+ ? `\n- Option strategy: ${s.optionStrategy}${tag(sources.optionStrategy)}`
267
+ : "",
268
+ s.costBasis !== undefined
269
+ ? `\n- Cost basis: ${formatBudget(s.costBasis)} (Position cost basis: ${formatBudget(s.costBasis)})${tag(sources.costBasis)}`
270
+ : "",
271
+ s.shareQuantity !== undefined
272
+ ? `\n- Share quantity: ${s.shareQuantity} shares${tag(sources.shareQuantity)}`
273
+ : "",
274
+ s.catalystSymbols?.length
275
+ ? `\n- Catalyst/context tickers: ${s.catalystSymbols.join(", ")}${tag(sources.catalystSymbols)}`
276
+ : "",
277
+ ].join("");
278
+
279
+ const isProtectivePutContext = s.optionStrategy === "protective_put";
280
+ const isCoveredCallContext =
281
+ !isProtectivePutContext &&
282
+ (s.optionStrategy === "covered_call" ||
283
+ s.costBasis !== undefined ||
284
+ (s.catalystSymbols?.length ?? 0) > 0);
285
+ const coveredCallInstructions = isCoveredCallContext
286
+ ? `
287
+ Covered-call sale guidance:
288
+ - Treat this as selling covered calls against an existing ${s.symbol} share position, not buying calls.
289
+ - Treat ${s.symbol} as the option-chain underlying.
290
+ - Because the user phrased ${s.symbol} as an existing holding, briefly state that you are treating ${s.symbol} as the held ticker. If they meant memory exposure or a different ticker, tell them to clarify and do not silently switch to another underlying.
291
+ - Do not substitute catalyst/context tickers as the option-chain underlying.
292
+ - Use catalyst/context tickers only to frame event risk, sympathy moves, and whether a nearer expiration is appropriate.
293
+ - Rank by premium collected, strike above cost basis, assignment risk, event risk, and live liquidity.
294
+ - Do not describe max loss as the option premium paid. Covered-call sale risks are assignment/capped upside, share-price downside in the owned stock, IV/event risk, and poor exit liquidity.
295
+ - If the option-chain tool reports closed_market_or_stale_quotes, do not treat zero bid/ask as confirmed live illiquidity; say the chain was checked outside regular options trading and recheck after regular options trading opens.
296
+ - If retrieved contracts have zero bid/ask, zero open interest, or otherwise unusable live quotes, the final answer MUST still include "Best action:" and "Conditional candidate:".
297
+ - In that fallback, "Best action:" should be no trade unless the user's broker shows a real bid, and "Conditional candidate:" should be a strike above cost basis labeled conditional on live bid/ask.
298
+ `
299
+ : "";
300
+ const protectivePutInstructions = isProtectivePutContext
301
+ ? `
302
+ Protective-put hedge guidance:
303
+ - Treat this as buying puts to hedge an existing long ${s.symbol} share position, not buying calls.
304
+ - Treat ${s.symbol} as the option-chain underlying.
305
+ - Rank put contracts by protection per dollar of premium: expiration fit, hedge floor, moneyness, liquidity, and premium as a percent of the stock position.
306
+ - For "doesn't cost too much" or similar cost-sensitive language, prefer liquid puts modestly below the current stock price before far-OTM lottery hedges; explain the tradeoff between cheaper premium and weaker protection.
307
+ - If share quantity is provided, use 1 put contract per 100 shares when discussing coverage and contract count.
308
+ - If share quantity is provided, state total premium for the required number of contracts in the ranked table or top-pick explanation.
309
+ - Include the hedge floor: approximate protected stock value at strike, net of premium where possible.
310
+ - Mention lower-cost alternatives such as a collar or put spread when outright put premium is high.
311
+ - Long protective puts have premium/decay risk and exercise/exit choices; do not frame assignment risk like a short option sale.
312
+ `
313
+ : "";
314
+ const topPickExplanation = isCoveredCallContext
315
+ ? `Explain why the top pick is ranked #1. For covered calls with a cost basis, include the effective assignment sale price (strike + premium collected) and compare it with the ${s.costBasis !== undefined ? formatBudget(s.costBasis) : "user's"} cost basis.`
316
+ : "Explain why the top pick is ranked #1.";
317
+
224
318
  return `Current date: ${dateStr}
225
319
  Do NOT invent or assume a different current date.${expirationSection}
226
320
 
@@ -229,51 +323,160 @@ Screen and rank options contracts for ${s.symbol}:
229
323
  - DTE target: ${s.dteTarget}${tag(sources.dteTarget)}
230
324
  - Objective: ${s.objective}${tag(sources.objective)}
231
325
  - Moneyness: ${s.moneynessPreference}${tag(sources.moneynessPreference)}
232
- - Liquidity: ${s.liquidityMinimum}${tag(sources.liquidityMinimum)}${s.budget ? `\n- Budget: ${formatBudget(s.budget)}` : ""}${s.maxPremium ? `\n- Max premium: ${formatBudget(s.maxPremium)}` : ""}
326
+ - Liquidity: ${s.liquidityMinimum}${tag(sources.liquidityMinimum)}${coveredCallContext}${s.budget ? `\n- Budget: ${formatBudget(s.budget)}` : ""}${s.maxPremium ? `\n- Max premium: ${formatBudget(s.maxPremium)}` : ""}
233
327
 
234
328
  Steps:
235
329
  1. Use get_stock_quote for ${s.symbol} to get current price and recent movement.
236
330
  2. Use get_option_chain for ${s.symbol} to get the full chain with Greeks. If you filter by contract type, pass \`type: "call"\` or \`type: "put"\` in lowercase.
237
- 3. Filter contracts matching: ${s.direction === "bullish" ? "calls" : "puts"}, DTE near ${s.dteTarget}, ${s.moneynessPreference} strikes.
238
- 4. Rank by ${s.objective}: balance premium cost, delta exposure, and probability of profit.
331
+ 3. Filter contracts matching: ${s.direction === "bullish" && !isProtectivePutContext ? "calls" : "puts"}, DTE near ${s.dteTarget}, ${s.moneynessPreference} strikes.
332
+ 4. ${isProtectivePutContext ? "Rank by hedge quality: protection per dollar of premium, expiration fit, moneyness, liquidity, and hedge floor." : `Rank by ${s.objective}: balance premium cost, delta exposure, and probability of profit.`}${s.maxPremium !== undefined ? ` Do not rank contracts above the user's max premium of ${formatBudget(s.maxPremium)} unless no contracts under that cap are liquid; if so, say the cap could not be met.` : ""}
239
333
  5. Filter for ${s.liquidityMinimum}: high open interest and tight bid-ask spread.
334
+ ${
335
+ s.optionStrategy === "covered_call"
336
+ ? `6. Covered call framing: treat option premium as premium received, not paid. Use the user's cost basis when provided, and include return-if-assigned and assignment/downside risk instead of long-call max-loss framing.
337
+ `
338
+ : ""
339
+ }${
340
+ isCoveredCallContext && s.costBasis !== undefined
341
+ ? `Cost-basis math: if assigned, share gain/loss is strike minus ${formatBudget(s.costBasis)} before premium. Total return if assigned is (strike - cost basis + premium received) / cost basis.
342
+ `
343
+ : ""
344
+ }
240
345
  ${longDatedInstructions}
346
+ ${coveredCallInstructions}
347
+ ${protectivePutInstructions}
241
348
  ${rankingConstraints}
242
349
  ${disclosureBlock}
243
350
 
244
351
  Response format:
245
352
  - Start with the assumptions block above exactly as written. Do not relabel source attribution anywhere else in your response.
246
- - Present top 3-5 ranked contracts in a table: strike, expiry, premium, delta, IV, OI, bid-ask spread.
247
- - Explain why the top pick is ranked #1.
248
- - Include risk caveats (max loss = premium, IV crush risk, time decay).`;
353
+ - ${isCoveredCallContext ? `Start with an Interpretation line: "Interpretation: Treating ${s.symbol} as the held ticker because you phrased it as an existing position. If you meant ${s.symbol} as memory exposure or another ticker, clarify before trading."` : isProtectivePutContext ? `Start with an Interpretation line: "Interpretation: Treating this as buying protective puts on an existing long ${s.symbol} share position."` : "State the interpretation only if the user's requested underlying is ambiguous."}
354
+ - Present top 3-5 ranked contracts in a table: strike, expiry, premium, delta, gamma, theta, vega, rho, IV, OI, bid-ask spread${isProtectivePutContext ? ", hedge floor, premium % of position" : ""}.
355
+ - ${topPickExplanation}
356
+ - Verify bid/ask and open interest in the user's broker before trading, even when OC shows live values.
357
+ - Include ${isCoveredCallContext ? "covered-call sale risks (assignment/capped upside, share-price downside in the owned stock, IV/event risk, exit liquidity). Do not describe max loss as the option premium paid" : isProtectivePutContext ? "protective-put risks (premium decay/cost, imperfect hedge before the strike, liquidity, and opportunity cost). Do not discuss short-option assignment risk" : "risk caveats (max loss = premium, IV crush risk, time decay)"}.`;
249
358
  }
250
359
 
251
360
  export function buildCompareAssetsPrompt(resolution: SlotResolution<CompareAssetsSlots>): string {
252
361
  const symbols = resolution.resolved.symbols;
253
362
  const symbolList = symbols.join(", ");
363
+ const timeHorizon = resolution.resolved.timeHorizon;
364
+ const budget = resolution.resolved.budget;
365
+ const includeSentiment = resolution.resolved.metrics?.includes("sentiment") ?? false;
366
+ const isMacroHedge = resolution.resolved.metrics?.includes("macro_hedge") ?? false;
367
+ const isInterestRateSensitive = resolution.resolved.metrics?.includes("interest_rates") ?? false;
368
+ const isOverlapComparison = resolution.resolved.metrics?.includes("overlap") ?? false;
369
+ const hasFundContext =
370
+ isFundOrIndexAssetScope(resolution.resolved.assetScope) || areLikelyFundOrIndexSymbols(symbols);
371
+ const shouldProbeFundOverlap =
372
+ !isOverlapComparison && isLongInvestmentHorizon(timeHorizon) && hasFundContext;
373
+ const sentimentStep = includeSentiment
374
+ ? `\n6. Use get_sentiment_summary for each of: ${symbolList} to compare retail/news sentiment and note source availability.`
375
+ : "";
376
+ const interestRateStep = isInterestRateSensitive
377
+ ? `\n${includeSentiment ? "7" : "6"}. Use get_economic_data for the current Fed funds backdrop. Treat this as historical/current context unless you also have explicit futures or forecast evidence.`
378
+ : "";
379
+ const sentimentMetric = includeSentiment ? ", sentiment score/summary" : "";
380
+ const interestRateGuidance = isInterestRateSensitive
381
+ ? `
382
+ interest-rate comparison guidance:
383
+ - Separate the user's conditional premise from observed data: if the prompt says rates "start falling," state that the recommendation depends on why rates fall and whether market pricing confirms it.
384
+ - Give a compact scenario split: benign disinflation/soft landing, recession or earnings shock, and sticky inflation or renewed rate pressure. State which asset type should benefit in each case and why.
385
+ - Connect rates to asset mechanics: duration-like sensitivity of future earnings, cost of capital, earnings resilience, valuation multiples, and risk appetite.
386
+ - For ETF or fund comparisons, include concentration and sector-exposure risk when one asset is meaningfully narrower or more growth/technology-heavy than the other.
387
+ - If forward valuation, earnings estimates, or rate-futures evidence is unavailable, say that directly and avoid treating historical Fed funds data as a forecast.`
388
+ : "";
389
+ const overlapGuidance = isOverlapComparison
390
+ ? `
391
+ ETF overlap guidance:
392
+ - Treat this as an ETF overlap and diversification question, not a generic return ranking.
393
+ - Lead with whether adding the second ETF creates a mega-cap technology tilt, growth-factor tilt, or real diversification.
394
+ - Explain that holdings overlap and sector concentration are not the same as correlation; correlation is supporting evidence, not the answer.
395
+ - Use provider top holdings and overlap weights when available. If provider coverage is partial or unavailable, say so directly and fall back to plain-language fund structure.
396
+ - Discuss top holdings, shared mega-cap names, sector concentration, and whether the position is a deliberate tilt or accidental duplication.
397
+ - avoid treating price, RSI, or generic risk metrics as the main answer.`
398
+ : shouldProbeFundOverlap
399
+ ? `
400
+ ETF/fund overlap check:
401
+ - If these assets are ETFs, funds, or index products, use provider-backed holdings-overlap evidence before making diversification claims.
402
+ - Compare fund role, style/factor tilt, concentration, and broad sector exposure when available; do not invent exact holdings or weights.
403
+ - For dividend/income funds versus growth funds over multi-year horizons, explain taxable account dividend drag: dividends can be taxed annually even when reinvested, while more return may be deferred as capital gains in growth-oriented funds. Contrast that with tax-advantaged accounts.
404
+ - Include expense ratios, dividend yields, and AUM only when fetched evidence supports them; otherwise tell the user to verify current fund facts before acting.
405
+ - Treat holdings overlap and sector concentration as different from correlation; correlation is supporting evidence, not a substitute for constituent exposure.
406
+ - If provider holdings coverage is partial or unavailable, say so directly and continue with the available price, risk, and correlation evidence.`
407
+ : "";
408
+ const macroHedgeSteps = isMacroHedge
409
+ ? `
410
+ macro hedge decision guidance:
411
+ - Treat this as a hedge-role comparison, not a generic "which asset has better recent technicals" ranking.
412
+ - Prioritize what each asset hedges: inflation, falling real yields, USD weakness, geopolitical/systemic shocks, liquidity stress, and risk-asset drawdowns.
413
+ - Compare volatility, drawdown, and correlation regime stability. If a metric is unavailable for one asset, explain how that limits confidence instead of awarding the other asset by default.
414
+ - Use current macro evidence where available: real yields or Fed-rate direction, inflation trend, USD/liquidity backdrop, and risk-on/risk-off conditions.
415
+ - Include a compact scenario map for stagflation, rising real yields, liquidity crunch/risk-off, USD debasement, and geopolitical shock. State which asset is likely the better hedge in each scenario and why.
416
+ - End with conditional guidance: prefer the steadier hedge for capital preservation; prefer the higher-volatility asset only for debasement/asymmetric-upside exposure.`
417
+ : "";
418
+ const tableInstruction = isMacroHedge
419
+ ? "- Present a comparison table with hedge-relevant columns: hedge role, macro drivers, volatility/drawdown evidence, correlation regime, liquidity/risk-on sensitivity, current data, and missing evidence."
420
+ : isOverlapComparison
421
+ ? "- Present an ETF overlap table with columns: fund role, shared top holdings/overlap weight from provider when available, sector concentration, what exposure is duplicated, what exposure is new, and diversification implication."
422
+ : shouldProbeFundOverlap
423
+ ? "- Present a long-horizon fund comparison table with columns: fund role/style, dividend/income versus growth tradeoff, risk evidence, holdings-overlap availability, tax and expense/yield/AUM verification gaps, and horizon fit."
424
+ : `- Present a comparison table with key metrics: price, P/E, revenue growth, profit margin, RSI, Sharpe, max drawdown${sentimentMetric}.
425
+ - Highlight which asset is stronger on each metric.`;
426
+ const technicalRiskSteps = isOverlapComparison
427
+ ? `3. Use analyze_holdings_overlap with symbols [${symbolList}] to fetch provider top holdings and compute pairwise overlap by weight.
428
+ 4. Use analyze_correlation across [${symbolList}] only as supporting diversification evidence; do not substitute correlation for holdings overlap.
429
+ 5. Skip momentum/risk tool calls unless the user asks about timing or trade setup; the core question is top holdings and sector overlap.`
430
+ : shouldProbeFundOverlap
431
+ ? `3. Use analyze_holdings_overlap with symbols [${symbolList}] to fetch provider top holdings and compute pairwise overlap by weight.
432
+ 4. Use analyze_correlation across [${symbolList}] as supporting diversification evidence.
433
+ 5. Use analyze_risk for each to compare long-horizon risk context.
434
+ 6. Use get_technical_indicators only as secondary timing context; do not let RSI or short-term momentum dominate the long-horizon fund decision.`
435
+ : `3. Use get_technical_indicators for each to compare momentum and trend.
436
+ 4. Use analyze_risk for each to compare risk metrics.
437
+ 5. Use analyze_correlation across [${symbolList}] to check diversification.`;
438
+ const horizonLine = timeHorizon ? `\nTime horizon: ${timeHorizon}` : "";
439
+ const budgetLine = budget !== undefined ? `\nBudget: ${formatBudget(budget)}` : "";
440
+ const horizonSteps = timeHorizon
441
+ ? `
442
+ 6. Adapt the comparison to the ${timeHorizon} horizon: prioritize near-term catalysts, earnings/guidance, estimate revisions, sentiment, and forward-looking valuation evidence over long-term historical averages.
443
+ 7. Use historical risk and technical metrics as context, but explain what they do and do not imply over ${timeHorizon}.`
444
+ : "";
445
+ const horizonResponse = timeHorizon
446
+ ? `
447
+ - Start the verdict by directly answering whether the assets should compare for a ${timeHorizon} horizon and why.
448
+ - Prioritize the evidence that matters most over ${timeHorizon}: near-term catalysts, earnings/guidance, forward-looking valuation/estimates, sentiment, macro sensitivity, and company-specific risks.
449
+ - Call out evidence that is missing or unavailable, especially forward-looking estimates or company-specific catalysts.`
450
+ : "";
254
451
 
255
452
  const disclosureBlock = buildDisclosureBlock(
256
- { symbols: symbolList },
453
+ {
454
+ symbols: symbolList,
455
+ ...(timeHorizon ? { timeHorizon } : {}),
456
+ ...(budget !== undefined ? { budget: formatBudget(budget) } : {}),
457
+ ...(resolution.resolved.assetScope ? { assetScope: resolution.resolved.assetScope } : {}),
458
+ ...(resolution.resolved.metrics ? { metrics: resolution.resolved.metrics.join(", ") } : {}),
459
+ },
257
460
  resolution.sources as Record<string, SlotSource | undefined>,
258
461
  );
259
462
 
260
463
  return `Current date: ${todayStr()}
261
464
 
262
- Compare these assets side by side: ${symbolList}
465
+ Compare these assets side by side: ${symbolList}${horizonLine}${budgetLine}
263
466
 
264
467
  Steps:
265
468
  1. Use get_stock_quote for each of: ${symbolList}.
266
469
  2. Use compare_companies with symbols [${symbols.map((s) => `"${s}"`).join(", ")}] for peer metrics. If some fundamentals are unavailable, continue the comparison with the available symbols and mark missing metrics as unavailable.
267
- 3. Use get_technical_indicators for each to compare momentum and trend.
268
- 4. Use analyze_risk for each to compare risk metrics.
269
- 5. Use analyze_correlation across [${symbolList}] to check diversification.
470
+ ${technicalRiskSteps}${sentimentStep}${interestRateStep}${horizonSteps}
471
+ ${macroHedgeSteps}
472
+ ${interestRateGuidance}
473
+ ${overlapGuidance}
270
474
 
271
475
  ${disclosureBlock}
272
476
 
273
477
  Response format:
274
478
  - Start with the assumptions block above exactly as written. Do not relabel source attribution anywhere else in your response.
275
- - Present a comparison table with key metrics: price, P/E, revenue growth, profit margin, RSI, Sharpe, max drawdown.
276
- - Highlight which asset is stronger on each metric.
479
+ ${tableInstruction}
277
480
  - Provide a summary verdict: which is most attractive and why.
278
- - Note any caveats (different sectors, market cap disparity, unavailable fundamentals, etc.).`;
481
+ - Note any caveats (different sectors, concentration, market cap disparity, unavailable fundamentals, unavailable forward-looking estimates, etc.).${horizonResponse}`;
279
482
  }
@@ -1,9 +1,9 @@
1
- import { httpGet, HttpError } from "../infra/http-client.js";
2
- import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
1
+ import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
2
+ import { HttpError, httpGet } from "../infra/http-client.js";
3
3
  import { rateLimiter } from "../infra/rate-limiter.js";
4
- import { ProviderCredentialError } from "./provider-credential-error.js";
5
4
  import type { CompanyOverview, EarningsData, FinancialStatement } from "../types/fundamentals.js";
6
- import type { StockQuote, OHLCV } from "../types/market.js";
5
+ import type { OHLCV, StockQuote } from "../types/market.js";
6
+ import { ProviderCredentialError } from "./provider-credential-error.js";
7
7
 
8
8
  const BASE_URL = "https://www.alphavantage.co/query";
9
9
  const MISSING_OVERVIEW_TTL = 15 * 60_000;
@@ -27,10 +27,24 @@ function buildUrl(fn: string, params: Record<string, string>, apiKey: string): s
27
27
  return `${BASE_URL}?${qs}`;
28
28
  }
29
29
 
30
- export async function getOverview(
31
- symbol: string,
32
- apiKey: string,
33
- ): Promise<CompanyOverview> {
30
+ function throwIfApiMessage(data: unknown): void {
31
+ if (!data || typeof data !== "object") return;
32
+
33
+ const payload = data as Record<string, unknown>;
34
+ const message = payload.Note ?? payload.Information ?? payload["Error Message"];
35
+ if (typeof message !== "string" || message.length === 0) return;
36
+
37
+ const normalized = message.toLowerCase();
38
+ if (normalized.includes("api call frequency") || normalized.includes("rate limit")) {
39
+ throw new Error(`Alpha Vantage rate limited: ${message}`);
40
+ }
41
+ if (normalized.includes("invalid api")) {
42
+ throw new ProviderCredentialError("alpha_vantage", "stale");
43
+ }
44
+ throw new Error(`Alpha Vantage error: ${message}`);
45
+ }
46
+
47
+ export async function getOverview(symbol: string, apiKey: string): Promise<CompanyOverview> {
34
48
  const cacheKey = `av:overview:${symbol}`;
35
49
  const missingCacheKey = `${cacheKey}:missing`;
36
50
  const cached = cache.get<CompanyOverview>(cacheKey);
@@ -44,6 +58,7 @@ export async function getOverview(
44
58
 
45
59
  const url = buildUrl("OVERVIEW", { symbol }, apiKey);
46
60
  const data = await httpGet<Record<string, string>>(url);
61
+ throwIfApiMessage(data);
47
62
 
48
63
  if (!data.Symbol) {
49
64
  cache.set(missingCacheKey, "missing", MISSING_OVERVIEW_TTL);
@@ -80,10 +95,7 @@ export async function getOverview(
80
95
  }
81
96
  }
82
97
 
83
- export async function getEarnings(
84
- symbol: string,
85
- apiKey: string,
86
- ): Promise<EarningsData> {
98
+ export async function getEarnings(symbol: string, apiKey: string): Promise<EarningsData> {
87
99
  const cacheKey = `av:earnings:${symbol}`;
88
100
  const cached = cache.get<EarningsData>(cacheKey);
89
101
  if (cached) return cached;
@@ -93,6 +105,7 @@ export async function getEarnings(
93
105
 
94
106
  const url = buildUrl("EARNINGS", { symbol }, apiKey);
95
107
  const data = await httpGet<{ quarterlyEarnings: any[] }>(url);
108
+ throwIfApiMessage(data);
96
109
 
97
110
  const quarterly = (data.quarterlyEarnings ?? []).slice(0, 8).map((e: any) => ({
98
111
  date: e.fiscalDateEnding,
@@ -113,31 +126,36 @@ export async function getEarnings(
113
126
  }
114
127
  }
115
128
 
116
- export async function getFinancials(
117
- symbol: string,
118
- apiKey: string,
119
- ): Promise<FinancialStatement[]> {
129
+ export async function getFinancials(symbol: string, apiKey: string): Promise<FinancialStatement[]> {
120
130
  const cacheKey = `av:financials:${symbol}`;
121
131
  const cached = cache.get<FinancialStatement[]>(cacheKey);
122
132
  if (cached) return cached;
123
133
 
124
134
  try {
125
135
  // Fetch sequentially to respect Alpha Vantage rate limits (5 req/min free tier)
126
- const incomeData = await fetchStatement<{ annualReports: any[] }>("INCOME_STATEMENT", symbol, apiKey);
127
- const balanceData = await fetchStatement<{ annualReports: any[] }>("BALANCE_SHEET", symbol, apiKey);
128
- const cashFlowData = await fetchStatement<{ annualReports: any[] }>("CASH_FLOW", symbol, apiKey);
136
+ const incomeData = await fetchStatement<{ annualReports: any[] }>(
137
+ "INCOME_STATEMENT",
138
+ symbol,
139
+ apiKey,
140
+ );
141
+ const balanceData = await fetchStatement<{ annualReports: any[] }>(
142
+ "BALANCE_SHEET",
143
+ symbol,
144
+ apiKey,
145
+ );
146
+ const cashFlowData = await fetchStatement<{ annualReports: any[] }>(
147
+ "CASH_FLOW",
148
+ symbol,
149
+ apiKey,
150
+ );
129
151
 
130
152
  const incomeReports = incomeData.annualReports ?? [];
131
153
  const balanceReports = balanceData.annualReports ?? [];
132
154
  const cashFlowReports = cashFlowData.annualReports ?? [];
133
155
 
134
156
  // Index balance sheet and cash flow by fiscal date for merging
135
- const balanceByDate = new Map(
136
- balanceReports.map((r: any) => [r.fiscalDateEnding, r]),
137
- );
138
- const cashFlowByDate = new Map(
139
- cashFlowReports.map((r: any) => [r.fiscalDateEnding, r]),
140
- );
157
+ const balanceByDate = new Map(balanceReports.map((r: any) => [r.fiscalDateEnding, r]));
158
+ const cashFlowByDate = new Map(cashFlowReports.map((r: any) => [r.fiscalDateEnding, r]));
141
159
 
142
160
  const statements = incomeReports.slice(0, 4).map((r: any) => {
143
161
  const balance = balanceByDate.get(r.fiscalDateEnding) ?? {};
@@ -178,13 +196,12 @@ export async function getFinancials(
178
196
  async function fetchStatement<T>(fn: string, symbol: string, apiKey: string): Promise<T> {
179
197
  await rateLimiter.acquire("alphavantage");
180
198
  const url = buildUrl(fn, { symbol }, apiKey);
181
- return httpGet<T>(url);
199
+ const data = await httpGet<T>(url);
200
+ throwIfApiMessage(data);
201
+ return data;
182
202
  }
183
203
 
184
- export async function getGlobalQuote(
185
- symbol: string,
186
- apiKey: string,
187
- ): Promise<StockQuote> {
204
+ export async function getGlobalQuote(symbol: string, apiKey: string): Promise<StockQuote> {
188
205
  const cacheKey = `av:globalquote:${symbol}`;
189
206
  const cached = cache.get<StockQuote>(cacheKey);
190
207
  if (cached) return cached;
@@ -194,6 +211,7 @@ export async function getGlobalQuote(
194
211
 
195
212
  const url = buildUrl("GLOBAL_QUOTE", { symbol }, apiKey);
196
213
  const data = await httpGet<{ "Global Quote": Record<string, string> }>(url);
214
+ throwIfApiMessage(data);
197
215
  const gq = data["Global Quote"];
198
216
 
199
217
  if (!gq || !gq["05. price"]) {
@@ -211,10 +229,10 @@ export async function getGlobalQuote(
211
229
  low: parseFloat(gq["04. low"]) || 0,
212
230
  previousClose: parseFloat(gq["08. previous close"]) || 0,
213
231
  volume: parseInt(gq["06. volume"], 10) || 0,
214
- marketCap: 0, // Not available from GLOBAL_QUOTE
215
- pe: null, // Not available from GLOBAL_QUOTE
216
- week52High: 0, // Not available from GLOBAL_QUOTE
217
- week52Low: 0, // Not available from GLOBAL_QUOTE
232
+ marketCap: 0, // Not available from GLOBAL_QUOTE
233
+ pe: null, // Not available from GLOBAL_QUOTE
234
+ week52High: 0, // Not available from GLOBAL_QUOTE
235
+ week52Low: 0, // Not available from GLOBAL_QUOTE
218
236
  timestamp: Date.now(),
219
237
  };
220
238
 
@@ -244,14 +262,17 @@ export async function getDailyHistory(
244
262
  const daysNeeded = rangeToDays(range);
245
263
  const outputsize = daysNeeded > 100 ? "full" : "compact";
246
264
  const url = buildUrl("TIME_SERIES_DAILY", { symbol, outputsize }, apiKey);
247
- const data = await httpGet<{ "Time Series (Daily)": Record<string, Record<string, string>> }>(url);
265
+ const data = await httpGet<{ "Time Series (Daily)": Record<string, Record<string, string>> }>(
266
+ url,
267
+ );
268
+ throwIfApiMessage(data);
248
269
 
249
270
  const timeSeries = data["Time Series (Daily)"];
250
271
  if (!timeSeries) {
251
272
  throw new Error(`Alpha Vantage: No daily history for ${symbol}`);
252
273
  }
253
274
 
254
- const ohlcv: OHLCV[] = Object.entries(timeSeries)
275
+ const sorted = Object.entries(timeSeries)
255
276
  .map(([date, bar]) => ({
256
277
  date,
257
278
  open: parseFloat(bar["1. open"]) || 0,
@@ -260,8 +281,14 @@ export async function getDailyHistory(
260
281
  close: parseFloat(bar["4. close"]) || 0,
261
282
  volume: parseInt(bar["5. volume"], 10) || 0,
262
283
  }))
263
- .sort((a, b) => a.date.localeCompare(b.date))
264
- .slice(-daysNeeded);
284
+ .sort((a, b) => a.date.localeCompare(b.date));
285
+
286
+ // Count-based slicing for ytd is only an estimate (ignores holidays and
287
+ // the starting weekday) and can leak prior-year bars; filter by date.
288
+ const ohlcv: OHLCV[] =
289
+ range === "ytd"
290
+ ? sorted.filter((bar) => bar.date >= `${new Date().getFullYear()}-01-01`)
291
+ : sorted.slice(-daysNeeded);
265
292
 
266
293
  cache.set(cacheKey, ohlcv, TTL.HISTORY);
267
294
  return ohlcv;
@@ -275,12 +302,27 @@ export async function getDailyHistory(
275
302
 
276
303
  function rangeToDays(range: string): number {
277
304
  const map: Record<string, number> = {
278
- "1d": 1, "5d": 5, "1mo": 22, "3mo": 66, "6mo": 130,
279
- "1y": 252, "2y": 504, "5y": 1260, "max": 5000,
305
+ "1d": 1,
306
+ "5d": 5,
307
+ "1mo": 22,
308
+ "3mo": 66,
309
+ "6mo": 130,
310
+ "1y": 252,
311
+ "2y": 504,
312
+ "5y": 1260,
313
+ "10y": 2520,
314
+ max: 5000,
280
315
  };
316
+ if (range === "ytd") return tradingDaysSinceStartOfYear();
281
317
  return map[range] ?? 130;
282
318
  }
283
319
 
320
+ function tradingDaysSinceStartOfYear(date = new Date()): number {
321
+ const start = new Date(date.getFullYear(), 0, 1);
322
+ const calendarDays = Math.max(1, Math.ceil((date.getTime() - start.getTime()) / 86_400_000) + 1);
323
+ return Math.max(1, Math.ceil((calendarDays / 7) * 5));
324
+ }
325
+
284
326
  function parseNum(s: string | undefined): number {
285
327
  return parseFloat(s ?? "0") || 0;
286
328
  }
@@ -1,5 +1,5 @@
1
+ import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
1
2
  import { httpGet } from "../infra/http-client.js";
2
- import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
3
3
  import { rateLimiter } from "../infra/rate-limiter.js";
4
4
  import type { CryptoPrice, OHLCV } from "../types/market.js";
5
5
 
@@ -63,10 +63,7 @@ export async function getCryptoPrice(id: string): Promise<CryptoPrice> {
63
63
  }
64
64
  }
65
65
 
66
- export async function getCryptoHistory(
67
- id: string,
68
- days: number = 180,
69
- ): Promise<OHLCV[]> {
66
+ export async function getCryptoHistory(id: string, days: number = 180): Promise<OHLCV[]> {
70
67
  const cacheKey = `coingecko:history:${id}:${days}`;
71
68
  const cached = cache.get<OHLCV[]>(cacheKey);
72
69
  if (cached) return cached;
@@ -0,0 +1,9 @@
1
+ export class InvalidSymbolError extends Error {
2
+ constructor(
3
+ public readonly symbol: string,
4
+ public readonly provider: string,
5
+ ) {
6
+ super(`Invalid symbol ${symbol} for ${provider}`);
7
+ this.name = "InvalidSymbolError";
8
+ }
9
+ }