opencandle 0.3.0 → 0.5.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 (408) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -14
  3. package/assets/logo.svg +187 -0
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.js +40 -3
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +25 -0
  8. package/dist/config.js +72 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/infra/browser.d.ts +11 -3
  11. package/dist/infra/browser.js +2 -1
  12. package/dist/infra/browser.js.map +1 -1
  13. package/dist/infra/native-dependencies.d.ts +1 -0
  14. package/dist/infra/native-dependencies.js +10 -0
  15. package/dist/infra/native-dependencies.js.map +1 -0
  16. package/dist/infra/node-version.d.ts +2 -0
  17. package/dist/infra/node-version.js +23 -0
  18. package/dist/infra/node-version.js.map +1 -0
  19. package/dist/infra/rate-limiter.d.ts +4 -0
  20. package/dist/infra/rate-limiter.js +5 -1
  21. package/dist/infra/rate-limiter.js.map +1 -1
  22. package/dist/memory/index.d.ts +2 -0
  23. package/dist/memory/index.js +1 -0
  24. package/dist/memory/index.js.map +1 -1
  25. package/dist/memory/manager.d.ts +9 -0
  26. package/dist/memory/manager.js +28 -11
  27. package/dist/memory/manager.js.map +1 -1
  28. package/dist/memory/sqlite.js +42 -4
  29. package/dist/memory/sqlite.js.map +1 -1
  30. package/dist/memory/storage.d.ts +7 -0
  31. package/dist/memory/storage.js +3 -3
  32. package/dist/memory/storage.js.map +1 -1
  33. package/dist/memory/tool-defaults.d.ts +8 -0
  34. package/dist/memory/tool-defaults.js +59 -0
  35. package/dist/memory/tool-defaults.js.map +1 -0
  36. package/dist/memory/types.js +4 -0
  37. package/dist/memory/types.js.map +1 -1
  38. package/dist/onboarding/connect.d.ts +13 -1
  39. package/dist/onboarding/connect.js +21 -10
  40. package/dist/onboarding/connect.js.map +1 -1
  41. package/dist/onboarding/prompt-user.d.ts +1 -1
  42. package/dist/onboarding/providers.d.ts +7 -0
  43. package/dist/onboarding/providers.js +6 -3
  44. package/dist/onboarding/providers.js.map +1 -1
  45. package/dist/onboarding/tool-helpers.d.ts +1 -1
  46. package/dist/pi/opencandle-extension.d.ts +7 -1
  47. package/dist/pi/opencandle-extension.js +391 -21
  48. package/dist/pi/opencandle-extension.js.map +1 -1
  49. package/dist/pi/session-storage.d.ts +2 -0
  50. package/dist/pi/session-storage.js +5 -0
  51. package/dist/pi/session-storage.js.map +1 -0
  52. package/dist/pi/session.d.ts +4 -1
  53. package/dist/pi/session.js +25 -3
  54. package/dist/pi/session.js.map +1 -1
  55. package/dist/pi/setup.d.ts +1 -1
  56. package/dist/pi/setup.js +11 -1
  57. package/dist/pi/setup.js.map +1 -1
  58. package/dist/pi/tool-adapter.d.ts +2 -2
  59. package/dist/pi/tool-adapter.js +14 -1
  60. package/dist/pi/tool-adapter.js.map +1 -1
  61. package/dist/prompts/context-builder.d.ts +40 -3
  62. package/dist/prompts/context-builder.js +140 -19
  63. package/dist/prompts/context-builder.js.map +1 -1
  64. package/dist/prompts/disclaimer.d.ts +6 -0
  65. package/dist/prompts/disclaimer.js +9 -0
  66. package/dist/prompts/disclaimer.js.map +1 -0
  67. package/dist/prompts/policy-cards.d.ts +13 -0
  68. package/dist/prompts/policy-cards.js +197 -0
  69. package/dist/prompts/policy-cards.js.map +1 -0
  70. package/dist/prompts/sections.js +3 -3
  71. package/dist/prompts/sections.js.map +1 -1
  72. package/dist/prompts/workflow-prompts.d.ts +8 -0
  73. package/dist/prompts/workflow-prompts.js +208 -22
  74. package/dist/prompts/workflow-prompts.js.map +1 -1
  75. package/dist/providers/alpha-vantage.js +23 -1
  76. package/dist/providers/alpha-vantage.js.map +1 -1
  77. package/dist/providers/sec-edgar.d.ts +8 -1
  78. package/dist/providers/sec-edgar.js +172 -5
  79. package/dist/providers/sec-edgar.js.map +1 -1
  80. package/dist/providers/yahoo-finance.d.ts +2 -0
  81. package/dist/providers/yahoo-finance.js +203 -35
  82. package/dist/providers/yahoo-finance.js.map +1 -1
  83. package/dist/routing/classify-intent.d.ts +3 -0
  84. package/dist/routing/classify-intent.js +82 -3
  85. package/dist/routing/classify-intent.js.map +1 -1
  86. package/dist/routing/defaults.js +4 -4
  87. package/dist/routing/defaults.js.map +1 -1
  88. package/dist/routing/entity-extractor.d.ts +1 -0
  89. package/dist/routing/entity-extractor.js +158 -12
  90. package/dist/routing/entity-extractor.js.map +1 -1
  91. package/dist/routing/index.d.ts +10 -0
  92. package/dist/routing/index.js +7 -0
  93. package/dist/routing/index.js.map +1 -1
  94. package/dist/routing/legacy-rule-router.d.ts +9 -0
  95. package/dist/routing/legacy-rule-router.js +12 -0
  96. package/dist/routing/legacy-rule-router.js.map +1 -0
  97. package/dist/routing/planning.d.ts +54 -0
  98. package/dist/routing/planning.js +531 -0
  99. package/dist/routing/planning.js.map +1 -0
  100. package/dist/routing/route-manifest.d.ts +35 -0
  101. package/dist/routing/route-manifest.js +221 -0
  102. package/dist/routing/route-manifest.js.map +1 -0
  103. package/dist/routing/router-llm-client.d.ts +11 -0
  104. package/dist/routing/router-llm-client.js +42 -0
  105. package/dist/routing/router-llm-client.js.map +1 -0
  106. package/dist/routing/router-prompt.d.ts +2 -0
  107. package/dist/routing/router-prompt.js +141 -0
  108. package/dist/routing/router-prompt.js.map +1 -0
  109. package/dist/routing/router-types.d.ts +71 -0
  110. package/dist/routing/router-types.js +2 -0
  111. package/dist/routing/router-types.js.map +1 -0
  112. package/dist/routing/router.d.ts +11 -0
  113. package/dist/routing/router.js +638 -0
  114. package/dist/routing/router.js.map +1 -0
  115. package/dist/routing/slot-resolver.js +46 -6
  116. package/dist/routing/slot-resolver.js.map +1 -1
  117. package/dist/routing/turn-context.d.ts +44 -0
  118. package/dist/routing/turn-context.js +45 -0
  119. package/dist/routing/turn-context.js.map +1 -0
  120. package/dist/routing/types.d.ts +13 -1
  121. package/dist/runtime/answer-contracts.d.ts +82 -0
  122. package/dist/runtime/answer-contracts.js +414 -0
  123. package/dist/runtime/answer-contracts.js.map +1 -0
  124. package/dist/runtime/artifact-contracts.d.ts +14 -0
  125. package/dist/runtime/artifact-contracts.js +57 -0
  126. package/dist/runtime/artifact-contracts.js.map +1 -0
  127. package/dist/runtime/planning-evidence.d.ts +99 -0
  128. package/dist/runtime/planning-evidence.js +445 -0
  129. package/dist/runtime/planning-evidence.js.map +1 -0
  130. package/dist/runtime/session-coordinator.d.ts +81 -3
  131. package/dist/runtime/session-coordinator.js +201 -17
  132. package/dist/runtime/session-coordinator.js.map +1 -1
  133. package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
  134. package/dist/runtime/tool-defaults-wrapper.js +25 -0
  135. package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
  136. package/dist/sentiment/store.js +5 -0
  137. package/dist/sentiment/store.js.map +1 -1
  138. package/dist/system-prompt.js +23 -12
  139. package/dist/system-prompt.js.map +1 -1
  140. package/dist/tool-kit.d.ts +4 -4
  141. package/dist/tools/fundamentals/company-overview.d.ts +1 -1
  142. package/dist/tools/fundamentals/company-overview.js +1 -1
  143. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  144. package/dist/tools/fundamentals/comps.d.ts +1 -1
  145. package/dist/tools/fundamentals/comps.js +1 -1
  146. package/dist/tools/fundamentals/comps.js.map +1 -1
  147. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  148. package/dist/tools/fundamentals/dcf.js +1 -1
  149. package/dist/tools/fundamentals/dcf.js.map +1 -1
  150. package/dist/tools/fundamentals/earnings.d.ts +1 -1
  151. package/dist/tools/fundamentals/earnings.js +1 -1
  152. package/dist/tools/fundamentals/earnings.js.map +1 -1
  153. package/dist/tools/fundamentals/financials.d.ts +1 -1
  154. package/dist/tools/fundamentals/financials.js +1 -1
  155. package/dist/tools/fundamentals/financials.js.map +1 -1
  156. package/dist/tools/fundamentals/sec-filings.d.ts +2 -1
  157. package/dist/tools/fundamentals/sec-filings.js +19 -2
  158. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  159. package/dist/tools/index.d.ts +29 -1
  160. package/dist/tools/index.js +30 -0
  161. package/dist/tools/index.js.map +1 -1
  162. package/dist/tools/interaction/ask-user.d.ts +1 -1
  163. package/dist/tools/interaction/twitter-login.d.ts +1 -1
  164. package/dist/tools/macro/fear-greed.d.ts +1 -1
  165. package/dist/tools/macro/fear-greed.js +1 -1
  166. package/dist/tools/macro/fear-greed.js.map +1 -1
  167. package/dist/tools/macro/fred-data.d.ts +1 -1
  168. package/dist/tools/macro/fred-data.js +29 -5
  169. package/dist/tools/macro/fred-data.js.map +1 -1
  170. package/dist/tools/market/crypto-history.d.ts +1 -1
  171. package/dist/tools/market/crypto-history.js +18 -2
  172. package/dist/tools/market/crypto-history.js.map +1 -1
  173. package/dist/tools/market/crypto-price.d.ts +1 -1
  174. package/dist/tools/market/crypto-price.js +1 -1
  175. package/dist/tools/market/crypto-price.js.map +1 -1
  176. package/dist/tools/market/search-ticker.d.ts +1 -1
  177. package/dist/tools/market/search-ticker.js +1 -1
  178. package/dist/tools/market/search-ticker.js.map +1 -1
  179. package/dist/tools/market/stock-history.d.ts +1 -1
  180. package/dist/tools/market/stock-history.js +1 -1
  181. package/dist/tools/market/stock-history.js.map +1 -1
  182. package/dist/tools/market/stock-quote.d.ts +1 -1
  183. package/dist/tools/market/stock-quote.js +1 -1
  184. package/dist/tools/market/stock-quote.js.map +1 -1
  185. package/dist/tools/options/greeks.js +0 -1
  186. package/dist/tools/options/greeks.js.map +1 -1
  187. package/dist/tools/options/option-chain.d.ts +1 -1
  188. package/dist/tools/options/option-chain.js +13 -5
  189. package/dist/tools/options/option-chain.js.map +1 -1
  190. package/dist/tools/portfolio/correlation.d.ts +1 -1
  191. package/dist/tools/portfolio/correlation.js +1 -1
  192. package/dist/tools/portfolio/correlation.js.map +1 -1
  193. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  194. package/dist/tools/portfolio/holdings-overlap.js +105 -0
  195. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  196. package/dist/tools/portfolio/predictions.d.ts +1 -1
  197. package/dist/tools/portfolio/predictions.js +1 -1
  198. package/dist/tools/portfolio/predictions.js.map +1 -1
  199. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  200. package/dist/tools/portfolio/risk-analysis.js +1 -1
  201. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  202. package/dist/tools/portfolio/tracker.d.ts +1 -1
  203. package/dist/tools/portfolio/tracker.js +1 -1
  204. package/dist/tools/portfolio/tracker.js.map +1 -1
  205. package/dist/tools/portfolio/watchlist.d.ts +1 -1
  206. package/dist/tools/portfolio/watchlist.js +12 -4
  207. package/dist/tools/portfolio/watchlist.js.map +1 -1
  208. package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
  209. package/dist/tools/sentiment/reddit-sentiment.js +1 -1
  210. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  211. package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
  212. package/dist/tools/sentiment/sentiment-summary.js +57 -2
  213. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  214. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  215. package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
  216. package/dist/tools/sentiment/twitter-sentiment.js +1 -1
  217. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  218. package/dist/tools/sentiment/web-search.d.ts +1 -1
  219. package/dist/tools/sentiment/web-search.js +32 -3
  220. package/dist/tools/sentiment/web-search.js.map +1 -1
  221. package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
  222. package/dist/tools/sentiment/web-sentiment.js +1 -1
  223. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  224. package/dist/tools/technical/backtest.d.ts +3 -3
  225. package/dist/tools/technical/backtest.js +41 -27
  226. package/dist/tools/technical/backtest.js.map +1 -1
  227. package/dist/tools/technical/indicators.d.ts +1 -1
  228. package/dist/tools/technical/indicators.js +8 -4
  229. package/dist/tools/technical/indicators.js.map +1 -1
  230. package/dist/types/options.d.ts +10 -0
  231. package/dist/types/portfolio.d.ts +27 -0
  232. package/dist/workflows/compare-assets.js +38 -2
  233. package/dist/workflows/compare-assets.js.map +1 -1
  234. package/dist/workflows/options-screener.js +94 -8
  235. package/dist/workflows/options-screener.js.map +1 -1
  236. package/dist/workflows/portfolio-builder.js +9 -5
  237. package/dist/workflows/portfolio-builder.js.map +1 -1
  238. package/gui/server/ask-user-bridge.ts +82 -0
  239. package/gui/server/background-quotes.ts +31 -0
  240. package/gui/server/chat-event-adapter.ts +142 -0
  241. package/gui/server/gui-session-manager.ts +5 -0
  242. package/gui/server/invoke-tool.ts +89 -0
  243. package/gui/server/live-chat-event-adapter.ts +181 -0
  244. package/gui/server/model-setup.ts +100 -0
  245. package/gui/server/package.json +5 -0
  246. package/gui/server/projector.ts +254 -0
  247. package/gui/server/prompt-observation.ts +61 -0
  248. package/gui/server/server.ts +703 -0
  249. package/gui/server/session-actions.ts +31 -0
  250. package/gui/server/session-entry-wait.ts +81 -0
  251. package/gui/server/tool-metadata.ts +88 -0
  252. package/gui/server/websocket.ts +128 -0
  253. package/gui/server/writer-lock.ts +118 -0
  254. package/gui/shared/chat-events.ts +118 -0
  255. package/gui/shared/event-reducer.ts +186 -0
  256. package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +1 -0
  257. package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
  258. package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
  259. package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
  260. package/gui/web/dist/index.html +17 -0
  261. package/package.json +50 -18
  262. package/src/analysts/contracts.ts +189 -0
  263. package/src/analysts/orchestrator.ts +300 -0
  264. package/src/cli.ts +206 -0
  265. package/src/config.ts +245 -0
  266. package/src/index.ts +5 -0
  267. package/src/infra/browser.ts +111 -0
  268. package/src/infra/cache.ts +103 -0
  269. package/src/infra/http-client.ts +68 -0
  270. package/src/infra/index.ts +18 -0
  271. package/src/infra/native-dependencies.ts +12 -0
  272. package/src/infra/node-version.ts +24 -0
  273. package/src/infra/open-url.ts +28 -0
  274. package/src/infra/opencandle-paths.ts +64 -0
  275. package/src/infra/rate-limiter.ts +73 -0
  276. package/src/memory/index.ts +10 -0
  277. package/src/memory/manager.ts +192 -0
  278. package/src/memory/preference-extractor.ts +106 -0
  279. package/src/memory/retrieval.ts +70 -0
  280. package/src/memory/sqlite.ts +172 -0
  281. package/src/memory/storage.ts +205 -0
  282. package/src/memory/tool-defaults.ts +87 -0
  283. package/src/memory/types.ts +71 -0
  284. package/src/onboarding/connect.ts +184 -0
  285. package/src/onboarding/credential-interceptor.ts +134 -0
  286. package/src/onboarding/degradation-accumulator.ts +79 -0
  287. package/src/onboarding/prompt-user.ts +85 -0
  288. package/src/onboarding/providers.ts +315 -0
  289. package/src/onboarding/state.ts +218 -0
  290. package/src/onboarding/tool-helpers.ts +111 -0
  291. package/src/onboarding/tool-tags.ts +201 -0
  292. package/src/onboarding/validation.ts +158 -0
  293. package/src/pi/opencandle-extension.ts +955 -0
  294. package/src/pi/session-storage.ts +5 -0
  295. package/src/pi/session.ts +81 -0
  296. package/src/pi/setup.ts +381 -0
  297. package/src/pi/tool-adapter.ts +36 -0
  298. package/src/prompts/context-builder.ts +315 -0
  299. package/src/prompts/disclaimer.ts +9 -0
  300. package/src/prompts/policy-cards.ts +220 -0
  301. package/src/prompts/sections.ts +46 -0
  302. package/src/prompts/workflow-prompts.ts +433 -0
  303. package/src/providers/alpha-vantage.ts +315 -0
  304. package/src/providers/coingecko.ts +96 -0
  305. package/src/providers/exa-search.ts +373 -0
  306. package/src/providers/fear-greed.ts +45 -0
  307. package/src/providers/finnhub.ts +124 -0
  308. package/src/providers/fred.ts +83 -0
  309. package/src/providers/index.ts +9 -0
  310. package/src/providers/provider-credential-error.ts +23 -0
  311. package/src/providers/reddit.ts +151 -0
  312. package/src/providers/sec-edgar.ts +312 -0
  313. package/src/providers/twitter.ts +173 -0
  314. package/src/providers/web-search.ts +293 -0
  315. package/src/providers/with-fallback.ts +41 -0
  316. package/src/providers/wrap-provider.ts +64 -0
  317. package/src/providers/yahoo-finance.ts +534 -0
  318. package/src/routing/classify-intent.ts +285 -0
  319. package/src/routing/defaults.ts +29 -0
  320. package/src/routing/entity-extractor.ts +291 -0
  321. package/src/routing/index.ts +70 -0
  322. package/src/routing/legacy-rule-router.ts +13 -0
  323. package/src/routing/planning.ts +732 -0
  324. package/src/routing/route-manifest.ts +287 -0
  325. package/src/routing/router-llm-client.ts +51 -0
  326. package/src/routing/router-prompt.ts +163 -0
  327. package/src/routing/router-types.ts +87 -0
  328. package/src/routing/router.ts +712 -0
  329. package/src/routing/slot-resolver.ts +190 -0
  330. package/src/routing/turn-context.ts +111 -0
  331. package/src/routing/types.ts +75 -0
  332. package/src/runtime/answer-contracts.ts +633 -0
  333. package/src/runtime/artifact-contracts.ts +76 -0
  334. package/src/runtime/evidence.ts +77 -0
  335. package/src/runtime/planning-evidence.ts +591 -0
  336. package/src/runtime/prompt-step.ts +75 -0
  337. package/src/runtime/provider-tracker.ts +40 -0
  338. package/src/runtime/run-context.ts +22 -0
  339. package/src/runtime/session-coordinator.ts +472 -0
  340. package/src/runtime/tool-defaults-wrapper.ts +35 -0
  341. package/src/runtime/validation.ts +214 -0
  342. package/src/runtime/workflow-events.ts +75 -0
  343. package/src/runtime/workflow-runner.ts +188 -0
  344. package/src/runtime/workflow-types.ts +102 -0
  345. package/src/sentiment/adapters/finnhub.ts +44 -0
  346. package/src/sentiment/adapters/reddit.ts +65 -0
  347. package/src/sentiment/adapters/twitter.ts +36 -0
  348. package/src/sentiment/adapters/web.ts +44 -0
  349. package/src/sentiment/index.ts +58 -0
  350. package/src/sentiment/keywords.ts +9 -0
  351. package/src/sentiment/pipeline.ts +68 -0
  352. package/src/sentiment/scorer.ts +78 -0
  353. package/src/sentiment/store.ts +260 -0
  354. package/src/sentiment/trends.ts +90 -0
  355. package/src/sentiment/types.ts +108 -0
  356. package/src/system-prompt.ts +118 -0
  357. package/src/tool-kit.ts +68 -0
  358. package/src/tools/AGENTS.md +36 -0
  359. package/src/tools/fundamentals/company-overview.ts +54 -0
  360. package/src/tools/fundamentals/comps.ts +156 -0
  361. package/src/tools/fundamentals/dcf.ts +267 -0
  362. package/src/tools/fundamentals/earnings.ts +47 -0
  363. package/src/tools/fundamentals/financials.ts +54 -0
  364. package/src/tools/fundamentals/sec-filings.ts +84 -0
  365. package/src/tools/index.ts +91 -0
  366. package/src/tools/interaction/ask-user.ts +81 -0
  367. package/src/tools/interaction/twitter-login.ts +93 -0
  368. package/src/tools/macro/fear-greed.ts +41 -0
  369. package/src/tools/macro/fred-data.ts +80 -0
  370. package/src/tools/market/crypto-history.ts +67 -0
  371. package/src/tools/market/crypto-price.ts +53 -0
  372. package/src/tools/market/search-ticker.ts +53 -0
  373. package/src/tools/market/stock-history.ts +79 -0
  374. package/src/tools/market/stock-quote.ts +64 -0
  375. package/src/tools/options/greeks.ts +81 -0
  376. package/src/tools/options/option-chain.ts +96 -0
  377. package/src/tools/portfolio/correlation.ts +162 -0
  378. package/src/tools/portfolio/holdings-overlap.ts +123 -0
  379. package/src/tools/portfolio/predictions.ts +253 -0
  380. package/src/tools/portfolio/risk-analysis.ts +134 -0
  381. package/src/tools/portfolio/tracker.ts +147 -0
  382. package/src/tools/portfolio/watchlist.ts +159 -0
  383. package/src/tools/sentiment/reddit-sentiment.ts +164 -0
  384. package/src/tools/sentiment/sentiment-summary.ts +316 -0
  385. package/src/tools/sentiment/sentiment-trend.ts +58 -0
  386. package/src/tools/sentiment/twitter-sentiment.ts +96 -0
  387. package/src/tools/sentiment/web-search.ts +183 -0
  388. package/src/tools/sentiment/web-sentiment.ts +76 -0
  389. package/src/tools/technical/backtest.ts +267 -0
  390. package/src/tools/technical/indicators.ts +256 -0
  391. package/src/types/fundamentals.ts +46 -0
  392. package/src/types/index.ts +20 -0
  393. package/src/types/macro.ts +27 -0
  394. package/src/types/market.ts +43 -0
  395. package/src/types/options.ts +52 -0
  396. package/src/types/portfolio.ts +73 -0
  397. package/src/types/sentiment.ts +70 -0
  398. package/src/workflows/compare-assets.ts +75 -0
  399. package/src/workflows/index.ts +4 -0
  400. package/src/workflows/options-screener.ts +127 -0
  401. package/src/workflows/portfolio-builder.ts +56 -0
  402. package/src/workflows/types.ts +4 -0
  403. package/dist/runtime/index.d.ts +0 -16
  404. package/dist/runtime/index.js +0 -10
  405. package/dist/runtime/index.js.map +0 -1
  406. package/dist/runtime/provider-ids.d.ts +0 -14
  407. package/dist/runtime/provider-ids.js +0 -14
  408. package/dist/runtime/provider-ids.js.map +0 -1
@@ -0,0 +1,134 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { getHistory } from "../../providers/yahoo-finance.js";
4
+ import { wrapProvider } from "../../providers/wrap-provider.js";
5
+ import type { RiskMetrics } from "../../types/portfolio.js";
6
+
7
+ const params = Type.Object({
8
+ symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT, SPY)" }),
9
+ period: Type.Optional(
10
+ Type.String({ description: "Historical period for analysis: 6mo, 1y, 2y. Default: 1y" }),
11
+ ),
12
+ });
13
+
14
+ export const riskAnalysisTool: AgentTool<typeof params, RiskMetrics> = {
15
+ name: "analyze_risk",
16
+ label: "Risk Analysis",
17
+ description:
18
+ "Compute risk metrics for a stock: annualized return, volatility, Sharpe ratio, max drawdown, and Value at Risk (95%). All computed locally from historical data.",
19
+ parameters: params,
20
+ async execute(_toolCallId, args) {
21
+ const symbol = args.symbol.toUpperCase();
22
+ const period = args.period ?? "1y";
23
+ const result = await wrapProvider("yahoo", () => getHistory(symbol, period, "1d"));
24
+ if (result.status === "unavailable") {
25
+ return {
26
+ content: [{ type: "text", text: `⚠ Risk analysis unavailable for ${symbol} (${result.reason}).` }],
27
+ details: null as any,
28
+ };
29
+ }
30
+ const bars = result.data;
31
+ const closes = bars.map((b) => b.close);
32
+
33
+ if (closes.length < 30) {
34
+ return {
35
+ content: [{ type: "text", text: `Insufficient data for risk analysis (need 30+ days, got ${closes.length})` }],
36
+ details: null as any,
37
+ };
38
+ }
39
+
40
+ const metrics = computeRiskMetrics(symbol, closes);
41
+
42
+ const text = [
43
+ `**${symbol} Risk Analysis** (${bars[0].date} to ${bars[bars.length - 1].date}, ${closes.length} days)`,
44
+ ``,
45
+ `Annualized Return: ${(metrics.annualizedReturn * 100).toFixed(2)}%`,
46
+ `Annualized Volatility: ${(metrics.annualizedVolatility * 100).toFixed(2)}%`,
47
+ `Sharpe Ratio: ${metrics.sharpeRatio.toFixed(2)} ${sharpeLabel(metrics.sharpeRatio)}`,
48
+ `Max Drawdown: ${(metrics.maxDrawdown * 100).toFixed(2)}%`,
49
+ `Value at Risk (95%, daily): ${(metrics.var95 * 100).toFixed(2)}%`,
50
+ ``,
51
+ riskSummary(metrics),
52
+ ].join("\n");
53
+
54
+ return { content: [{ type: "text", text }], details: metrics };
55
+ },
56
+ };
57
+
58
+ export function computeRiskMetrics(symbol: string, closes: number[]): RiskMetrics {
59
+ const dailyReturns = computeDailyReturns(closes);
60
+ const avgDailyReturn = mean(dailyReturns);
61
+ const dailyVol = stddev(dailyReturns);
62
+
63
+ const annualizedReturn = avgDailyReturn * 252;
64
+ const annualizedVolatility = dailyVol * Math.sqrt(252);
65
+
66
+ // Sharpe ratio (assuming 5% risk-free rate)
67
+ const riskFreeDaily = 0.05 / 252;
68
+ const sharpeRatio =
69
+ dailyVol === 0 ? 0 : ((avgDailyReturn - riskFreeDaily) / dailyVol) * Math.sqrt(252);
70
+
71
+ const maxDrawdown = computeMaxDrawdown(closes);
72
+ const var95 = computeVaR(dailyReturns, 0.05);
73
+
74
+ return {
75
+ symbol,
76
+ annualizedReturn,
77
+ annualizedVolatility,
78
+ sharpeRatio,
79
+ maxDrawdown,
80
+ var95,
81
+ };
82
+ }
83
+
84
+ export function computeDailyReturns(prices: number[]): number[] {
85
+ const returns: number[] = [];
86
+ for (let i = 1; i < prices.length; i++) {
87
+ returns.push((prices[i] - prices[i - 1]) / prices[i - 1]);
88
+ }
89
+ return returns;
90
+ }
91
+
92
+ export function computeMaxDrawdown(prices: number[]): number {
93
+ let peak = prices[0];
94
+ let maxDd = 0;
95
+ for (const price of prices) {
96
+ if (price > peak) peak = price;
97
+ const dd = (peak - price) / peak;
98
+ if (dd > maxDd) maxDd = dd;
99
+ }
100
+ return maxDd;
101
+ }
102
+
103
+ export function computeVaR(returns: number[], confidence: number): number {
104
+ const sorted = [...returns].sort((a, b) => a - b);
105
+ const idx = Math.floor(sorted.length * confidence);
106
+ return Math.abs(sorted[idx]);
107
+ }
108
+
109
+ function mean(arr: number[]): number {
110
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
111
+ }
112
+
113
+ function stddev(arr: number[]): number {
114
+ const m = mean(arr);
115
+ const variance = arr.reduce((sum, val) => sum + (val - m) ** 2, 0) / arr.length;
116
+ return Math.sqrt(variance);
117
+ }
118
+
119
+ function sharpeLabel(s: number): string {
120
+ if (s >= 2) return "(Excellent)";
121
+ if (s >= 1) return "(Good)";
122
+ if (s >= 0) return "(Below average)";
123
+ return "(Negative — losing money)";
124
+ }
125
+
126
+ function riskSummary(m: RiskMetrics): string {
127
+ const signals: string[] = [];
128
+ if (m.annualizedVolatility > 0.4) signals.push("High volatility stock");
129
+ if (m.maxDrawdown > 0.3) signals.push("Large historical drawdown (>30%)");
130
+ if (m.sharpeRatio < 0) signals.push("Negative risk-adjusted returns");
131
+ if (m.sharpeRatio >= 1.5) signals.push("Strong risk-adjusted performance");
132
+ if (m.var95 > 0.03) signals.push("High daily VaR (>3%)");
133
+ return signals.length > 0 ? "Flags: " + signals.join(" | ") : "Risk profile appears moderate";
134
+ }
@@ -0,0 +1,147 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
4
+ import { getQuote } from "../../providers/yahoo-finance.js";
5
+ import { wrapProvider } from "../../providers/wrap-provider.js";
6
+ import type { Position, PortfolioSummary } from "../../types/portfolio.js";
7
+ import { ensureParentDir, getPortfolioPath } from "../../infra/opencandle-paths.js";
8
+
9
+ function loadPortfolio(): Position[] {
10
+ const portfolioPath = getPortfolioPath();
11
+ if (!existsSync(portfolioPath)) return [];
12
+ try {
13
+ return JSON.parse(readFileSync(portfolioPath, "utf-8"));
14
+ } catch {
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function savePortfolio(positions: Position[]): void {
20
+ const portfolioPath = getPortfolioPath();
21
+ ensureParentDir(portfolioPath);
22
+ writeFileSync(portfolioPath, JSON.stringify(positions, null, 2));
23
+ }
24
+
25
+ async function getCurrentPrice(symbol: string): Promise<number | null> {
26
+ const result = await wrapProvider("yahoo", () => getQuote(symbol));
27
+ if (result.status === "unavailable") return null;
28
+ return result.data.price;
29
+ }
30
+
31
+ const params = Type.Object({
32
+ action: Type.Union([
33
+ Type.Literal("add"),
34
+ Type.Literal("remove"),
35
+ Type.Literal("view"),
36
+ ], { description: "Action: add a position, remove a position, or view portfolio" }),
37
+ symbol: Type.Optional(
38
+ Type.String({ description: "Ticker symbol — stocks (AAPL, MSFT) or crypto with -USD suffix (BTC-USD, ETH-USD, SOL-USD). Use search_ticker to find the right ticker." }),
39
+ ),
40
+ shares: Type.Optional(
41
+ Type.Number({ description: "Number of shares/units (required for add)" }),
42
+ ),
43
+ avg_cost: Type.Optional(
44
+ Type.Number({ description: "Average cost per share/unit in USD (required for add)" }),
45
+ ),
46
+ });
47
+
48
+ export const portfolioTrackerTool: AgentTool<typeof params, PortfolioSummary | null> = {
49
+ name: "track_portfolio",
50
+ label: "Portfolio Tracker",
51
+ description:
52
+ "Track your portfolio of stocks and crypto. Add/remove positions with cost basis, or view current holdings with live P&L. For stocks use standard tickers (AAPL, MSFT). For crypto use the -USD suffix (BTC-USD, ETH-USD, SOL-USD). Use search_ticker first if you're unsure of the exact ticker. Data persisted to ~/.opencandle/portfolio.json.",
53
+ parameters: params,
54
+ async execute(_toolCallId, args) {
55
+ const positions = loadPortfolio();
56
+
57
+ if (args.action === "add") {
58
+ if (!args.symbol || !args.shares || !args.avg_cost) {
59
+ throw new Error("symbol, shares, and avg_cost are required for add action.");
60
+ }
61
+ const symbol = args.symbol.toUpperCase();
62
+ const existing = positions.find((p) => p.symbol === symbol);
63
+ if (existing) {
64
+ const totalShares = existing.shares + args.shares;
65
+ existing.avgCost =
66
+ (existing.avgCost * existing.shares + args.avg_cost * args.shares) / totalShares;
67
+ existing.shares = totalShares;
68
+ } else {
69
+ positions.push({
70
+ symbol,
71
+ shares: args.shares,
72
+ avgCost: args.avg_cost,
73
+ addedAt: new Date().toISOString(),
74
+ });
75
+ }
76
+ savePortfolio(positions);
77
+ return {
78
+ content: [{ type: "text", text: `Added ${args.shares} shares of ${symbol} at $${args.avg_cost.toFixed(2)}` }],
79
+ details: null,
80
+ };
81
+ }
82
+
83
+ if (args.action === "remove") {
84
+ if (!args.symbol) {
85
+ throw new Error("symbol is required for remove action.");
86
+ }
87
+ const symbol = args.symbol.toUpperCase();
88
+ const idx = positions.findIndex((p) => p.symbol === symbol);
89
+ if (idx === -1) {
90
+ return {
91
+ content: [{ type: "text", text: `${symbol} not found in portfolio` }],
92
+ details: null,
93
+ };
94
+ }
95
+ positions.splice(idx, 1);
96
+ savePortfolio(positions);
97
+ return {
98
+ content: [{ type: "text", text: `Removed ${symbol} from portfolio` }],
99
+ details: null,
100
+ };
101
+ }
102
+
103
+ // View portfolio
104
+ if (positions.length === 0) {
105
+ return {
106
+ content: [{ type: "text", text: "Portfolio is empty. Use add action to add positions." }],
107
+ details: null,
108
+ };
109
+ }
110
+
111
+ const enriched = await Promise.all(
112
+ positions.map(async (p) => {
113
+ const currentPrice = await getCurrentPrice(p.symbol) ?? p.avgCost;
114
+ const marketValue = currentPrice * p.shares;
115
+ const totalCost = p.avgCost * p.shares;
116
+ return {
117
+ ...p,
118
+ currentPrice,
119
+ marketValue,
120
+ totalCost,
121
+ pnl: marketValue - totalCost,
122
+ pnlPercent: ((marketValue - totalCost) / totalCost) * 100,
123
+ };
124
+ }),
125
+ );
126
+
127
+ const totalValue = enriched.reduce((s, p) => s + p.marketValue, 0);
128
+ const totalCost = enriched.reduce((s, p) => s + p.totalCost, 0);
129
+
130
+ const summary: PortfolioSummary = {
131
+ positions: enriched,
132
+ totalValue,
133
+ totalCost,
134
+ totalPnl: totalValue - totalCost,
135
+ totalPnlPercent: totalCost > 0 ? ((totalValue - totalCost) / totalCost) * 100 : 0,
136
+ };
137
+
138
+ const header = `**Portfolio** — ${enriched.length} positions | Value: $${totalValue.toFixed(2)} | P&L: $${summary.totalPnl.toFixed(2)} (${summary.totalPnlPercent >= 0 ? "+" : ""}${summary.totalPnlPercent.toFixed(2)}%)`;
139
+ const rows = enriched.map((p) => {
140
+ const sign = p.pnlPercent >= 0 ? "+" : "";
141
+ return ` ${p.symbol}: ${p.shares} @ $${p.avgCost.toFixed(2)} → $${p.currentPrice.toFixed(2)} | P&L: $${p.pnl.toFixed(2)} (${sign}${p.pnlPercent.toFixed(2)}%)`;
142
+ });
143
+
144
+ const text = [header, ...rows].join("\n");
145
+ return { content: [{ type: "text", text }], details: summary };
146
+ },
147
+ };
@@ -0,0 +1,159 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
4
+ import { getQuote } from "../../providers/yahoo-finance.js";
5
+ import { wrapProvider } from "../../providers/wrap-provider.js";
6
+ import { ensureParentDir, getWatchlistPath } from "../../infra/opencandle-paths.js";
7
+
8
+ interface WatchlistItem {
9
+ symbol: string;
10
+ addedAt: string;
11
+ targetPrice?: number;
12
+ stopPrice?: number;
13
+ notes?: string;
14
+ }
15
+
16
+ function loadWatchlist(): WatchlistItem[] {
17
+ const watchlistPath = getWatchlistPath();
18
+ if (!existsSync(watchlistPath)) return [];
19
+ try {
20
+ return JSON.parse(readFileSync(watchlistPath, "utf-8"));
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ function saveWatchlist(items: WatchlistItem[]): void {
27
+ const watchlistPath = getWatchlistPath();
28
+ ensureParentDir(watchlistPath);
29
+ writeFileSync(watchlistPath, JSON.stringify(items, null, 2));
30
+ }
31
+
32
+ const params = Type.Object({
33
+ action: Type.Union(
34
+ [Type.Literal("add"), Type.Literal("remove"), Type.Literal("check")],
35
+ { description: "One of: 'add', 'remove', or 'check'" },
36
+ ),
37
+ symbol: Type.Optional(
38
+ Type.String({ description: "Ticker symbol (required for add/remove)" }),
39
+ ),
40
+ target_price: Type.Optional(
41
+ Type.Number({ description: "Alert when price rises above this level" }),
42
+ ),
43
+ stop_price: Type.Optional(
44
+ Type.Number({ description: "Alert when price falls below this level" }),
45
+ ),
46
+ notes: Type.Optional(
47
+ Type.String({ description: "Optional notes for why you're watching this" }),
48
+ ),
49
+ });
50
+
51
+ export const watchlistTool: AgentTool<typeof params> = {
52
+ name: "manage_watchlist",
53
+ label: "Watchlist",
54
+ description:
55
+ "Manage your watchlist of stocks and crypto. Add symbols with optional target and stop prices, remove symbols, or check current prices against your alert levels. Data persisted to ~/.opencandle/watchlist.json.",
56
+ parameters: params,
57
+ async execute(_toolCallId, args) {
58
+ const items = loadWatchlist();
59
+
60
+ if (args.action === "add") {
61
+ if (!args.symbol) {
62
+ throw new Error("symbol is required for add action.");
63
+ }
64
+ const symbol = args.symbol.toUpperCase();
65
+ const existing = items.findIndex((i) => i.symbol === symbol);
66
+ const item: WatchlistItem = {
67
+ symbol,
68
+ addedAt: new Date().toISOString(),
69
+ ...(args.target_price != null && { targetPrice: args.target_price }),
70
+ ...(args.stop_price != null && { stopPrice: args.stop_price }),
71
+ ...(args.notes != null && { notes: args.notes }),
72
+ };
73
+ if (existing >= 0) {
74
+ items[existing] = item;
75
+ } else {
76
+ items.push(item);
77
+ }
78
+ saveWatchlist(items);
79
+ const alerts = [];
80
+ if (args.target_price) alerts.push(`target: $${args.target_price}`);
81
+ if (args.stop_price) alerts.push(`stop: $${args.stop_price}`);
82
+ const alertStr = alerts.length > 0 ? ` (${alerts.join(", ")})` : "";
83
+ return {
84
+ content: [{ type: "text", text: `Added ${symbol} to watchlist${alertStr}` }],
85
+ details: null,
86
+ };
87
+ }
88
+
89
+ if (args.action === "remove") {
90
+ if (!args.symbol) {
91
+ throw new Error("symbol is required for remove action.");
92
+ }
93
+ const symbol = args.symbol.toUpperCase();
94
+ const idx = items.findIndex((i) => i.symbol === symbol);
95
+ if (idx === -1) {
96
+ return {
97
+ content: [{ type: "text", text: `${symbol} not found in watchlist` }],
98
+ details: null,
99
+ };
100
+ }
101
+ items.splice(idx, 1);
102
+ saveWatchlist(items);
103
+ return {
104
+ content: [{ type: "text", text: `Removed ${symbol} from watchlist` }],
105
+ details: null,
106
+ };
107
+ }
108
+
109
+ // Check action
110
+ if (items.length === 0) {
111
+ return {
112
+ content: [{ type: "text", text: "Watchlist is empty. Use add action to add symbols." }],
113
+ details: null,
114
+ };
115
+ }
116
+
117
+ const checks = await Promise.all(
118
+ items.map(async (item) => {
119
+ const result = await wrapProvider("yahoo", () => getQuote(item.symbol));
120
+ if (result.status === "unavailable") {
121
+ return { ...item, currentPrice: 0, alerts: [`UNAVAILABLE: ${result.reason}`], statuses: [] };
122
+ }
123
+ const quote = result.data;
124
+ const alerts: string[] = [];
125
+ const statuses: string[] = [];
126
+ if (item.targetPrice && quote.price >= item.targetPrice) {
127
+ alerts.push(`TARGET HIT: $${quote.price.toFixed(2)} >= $${item.targetPrice}`);
128
+ } else if (item.targetPrice) {
129
+ statuses.push(`Target pending: $${quote.price.toFixed(2)} < $${item.targetPrice}`);
130
+ }
131
+ if (item.stopPrice && quote.price <= item.stopPrice) {
132
+ alerts.push(`STOP ALERT: $${quote.price.toFixed(2)} fell below $${item.stopPrice}`);
133
+ } else if (item.stopPrice) {
134
+ statuses.push(`Stop OK: $${quote.price.toFixed(2)} > $${item.stopPrice}`);
135
+ }
136
+ return { ...item, currentPrice: quote.price, alerts, statuses };
137
+ }),
138
+ );
139
+
140
+ const alertItems = checks.filter((c) => c.alerts.length > 0);
141
+ const lines = [
142
+ `**Watchlist** — ${items.length} symbols${alertItems.length > 0 ? ` | ${alertItems.length} ALERT(S)` : ""}`,
143
+ "",
144
+ ];
145
+
146
+ for (const c of checks) {
147
+ const alertStr = c.alerts.length > 0 ? ` ** ${c.alerts.join(" | ")} **` : "";
148
+ const statusStr = c.statuses.length > 0 ? ` | ${c.statuses.join(" | ")}` : "";
149
+ const targetStr = c.targetPrice ? ` | Target: $${c.targetPrice}` : "";
150
+ const stopStr = c.stopPrice ? ` | Stop: $${c.stopPrice}` : "";
151
+ lines.push(` ${c.symbol}: $${c.currentPrice.toFixed(2)}${targetStr}${stopStr}${statusStr}${alertStr}`);
152
+ }
153
+
154
+ return {
155
+ content: [{ type: "text", text: lines.join("\n") }],
156
+ details: { items: checks },
157
+ };
158
+ },
159
+ };
@@ -0,0 +1,164 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { getSubredditPosts, getPostComments } from "../../providers/reddit.js";
4
+ import { wrapProvider } from "../../providers/wrap-provider.js";
5
+ import type { RedditSentimentResult } from "../../types/sentiment.js";
6
+ import { RedditAdapter } from "../../sentiment/adapters/reddit.js";
7
+ import { getSentimentPipeline } from "../../sentiment/index.js";
8
+ import { getConfig } from "../../config.js";
9
+
10
+ const params = Type.Object({
11
+ subreddit: Type.Optional(
12
+ Type.String({
13
+ description:
14
+ "Subreddit name (e.g. wallstreetbets, stocks). If omitted, searches across default subreddits.",
15
+ }),
16
+ ),
17
+ query: Type.Optional(
18
+ Type.String({
19
+ description:
20
+ "Topic or ticker to filter posts by (e.g. AAPL, bitcoin). Searches titles and post bodies.",
21
+ }),
22
+ ),
23
+ subreddits: Type.Optional(
24
+ Type.Array(Type.String(), {
25
+ description: "Multiple subreddits to search. Overrides single subreddit param.",
26
+ }),
27
+ ),
28
+ limit: Type.Optional(
29
+ Type.Number({ description: "Number of posts per subreddit. Default: 25, max: 100" }),
30
+ ),
31
+ });
32
+
33
+ export const redditSentimentTool: AgentTool<typeof params, RedditSentimentResult> = {
34
+ name: "get_reddit_sentiment",
35
+ label: "Reddit Sentiment",
36
+ description:
37
+ "Analyze sentiment from financial Reddit communities. Supports single subreddit, multi-subreddit, and topic filtering. Returns scored posts with comment analysis and trend context.",
38
+ parameters: params,
39
+ async execute(_toolCallId, args) {
40
+ const limit = Math.min(args.limit ?? 25, 100);
41
+ const config = getConfig();
42
+
43
+ // Determine subreddits to search
44
+ let subreddits: string[];
45
+ if (args.subreddits && args.subreddits.length > 0) {
46
+ subreddits = args.subreddits;
47
+ } else if (args.subreddit) {
48
+ subreddits = [args.subreddit];
49
+ } else {
50
+ subreddits = config.sentiment?.defaultSubreddits ?? ["wallstreetbets", "stocks", "investing", "options"];
51
+ }
52
+
53
+ // Fetch from all subreddits
54
+ const allResults: RedditSentimentResult[] = [];
55
+ const warnings: string[] = [];
56
+ for (const sub of subreddits) {
57
+ const providerResult = await wrapProvider("reddit", () => getSubredditPosts(sub, limit));
58
+ if (providerResult.status === "unavailable") {
59
+ warnings.push(`r/${sub}: ${providerResult.reason}`);
60
+ continue;
61
+ }
62
+ allResults.push(providerResult.data);
63
+ }
64
+
65
+ if (allResults.length === 0) {
66
+ return {
67
+ content: [{ type: "text", text: `⚠ Reddit sentiment unavailable (${warnings.join("; ")}).` }],
68
+ details: null as any,
69
+ };
70
+ }
71
+
72
+ // Merge and filter by query if provided
73
+ const adapter = new RedditAdapter();
74
+ let allRecords = allResults.flatMap((r) => adapter.mapPostsToRecords(r, args.query ?? subreddits.join("+")));
75
+
76
+ // Topic filtering
77
+ if (args.query) {
78
+ const queryLower = args.query.toLowerCase();
79
+ allRecords = allRecords.filter((r) =>
80
+ r.text.toLowerCase().includes(queryLower) ||
81
+ (r.title?.toLowerCase().includes(queryLower) ?? false),
82
+ );
83
+ }
84
+
85
+ // Deduplicate by sourceId (crossposts)
86
+ const seen = new Set<string>();
87
+ allRecords = allRecords.filter((r) => {
88
+ if (seen.has(r.sourceId)) return false;
89
+ seen.add(r.sourceId);
90
+ return true;
91
+ });
92
+
93
+ // Fetch comments for top 10 posts by engagement
94
+ const commentsPerPost = config.sentiment?.commentsPerPost ?? 5;
95
+ const topPosts = [...allRecords]
96
+ .sort((a, b) => b.engagement.score - a.engagement.score)
97
+ .slice(0, 10);
98
+
99
+ for (const post of topPosts) {
100
+ const sub = (post.metadata.subreddit as string) ?? subreddits[0];
101
+ if ((post.engagement.replies ?? 0) === 0) continue;
102
+ try {
103
+ const comments = await getPostComments(sub, post.sourceId, commentsPerPost);
104
+ const commentRecords = adapter.mapCommentsToRecords(
105
+ comments,
106
+ post.sourceId,
107
+ sub,
108
+ args.query ?? subreddits.join("+"),
109
+ );
110
+ allRecords.push(...commentRecords);
111
+ } catch {
112
+ // Comment fetch failures are non-fatal
113
+ }
114
+ }
115
+
116
+ // Process through pipeline
117
+ const pipeline = getSentimentPipeline();
118
+ const pipelineResult = await pipeline.processRecords(allRecords, args.query ?? subreddits.join("+"));
119
+
120
+ // Build output using first result as base for backward compatibility
121
+ const firstResult = allResults[0];
122
+ const postRecords = pipelineResult.fresh.filter((r) => !r.metadata.isComment);
123
+ const commentRecords = pipelineResult.fresh.filter((r) => r.metadata.isComment);
124
+ const avgScore = postRecords.length > 0
125
+ ? postRecords.reduce((s, r) => s + r.sentiment.score, 0) / postRecords.length
126
+ : 0;
127
+
128
+ const sentimentLabel =
129
+ avgScore > 0.3 ? "Bullish" :
130
+ avgScore < -0.3 ? "Bearish" :
131
+ avgScore > 0 ? "Leaning Bullish" :
132
+ avgScore < 0 ? "Leaning Bearish" : "Neutral";
133
+
134
+ const subLabel = subreddits.length === 1 ? `r/${subreddits[0]}` : `${subreddits.length} subreddits`;
135
+ const lines = [
136
+ `**Reddit: ${args.query ?? subLabel}** — ${postRecords.length} posts, ${commentRecords.length} comments`,
137
+ `Sentiment: ${avgScore.toFixed(2)} (${sentimentLabel})`,
138
+ ];
139
+
140
+ if (firstResult.topMentions.length > 0) {
141
+ lines.push(`Tickers: ${firstResult.topMentions.map((t) => `$${t}`).join(", ")}`);
142
+ }
143
+
144
+ lines.push("");
145
+ lines.push("Top posts:");
146
+ for (const post of postRecords.slice(0, 10)) {
147
+ const scoreIndicator = post.sentiment.score > 0 ? "🟢" : post.sentiment.score < 0 ? "🔴" : "⚪";
148
+ lines.push(` ${scoreIndicator} ⬆${post.engagement.score} 💬${post.engagement.replies ?? 0} — ${(post.title ?? post.text).slice(0, 100)}`);
149
+ }
150
+
151
+ if (pipelineResult.trend && pipelineResult.trend.length > 0) {
152
+ const t = pipelineResult.trend[0];
153
+ lines.push("");
154
+ lines.push(`Trend: ${t.sparkline} ${t.direction} (${t.delta >= 0 ? "+" : ""}${t.delta.toFixed(2)}, ${t.count} records)`);
155
+ }
156
+
157
+ if (warnings.length > 0) {
158
+ lines.push("");
159
+ lines.push(`⚠ ${warnings.join("; ")}`);
160
+ }
161
+
162
+ return { content: [{ type: "text", text: lines.join("\n") }], details: firstResult };
163
+ },
164
+ };