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,93 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import type { BrowserContext } from "playwright-core";
3
+ import { Type } from "@sinclair/typebox";
4
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
+
6
+ interface TwitterLoginResult {
7
+ success: boolean;
8
+ message: string;
9
+ }
10
+
11
+ export async function runTwitterLogin(notify: (msg: string) => void): Promise<TwitterLoginResult> {
12
+ const { getBrowserProfileDir } = await import("../../infra/opencandle-paths.js");
13
+ const { Camoufox } = await import("camoufox-js");
14
+
15
+ const profileDir = getBrowserProfileDir();
16
+ mkdirSync(profileDir, { recursive: true });
17
+
18
+ notify("Launching browser for Twitter login...");
19
+
20
+ const context = (await Camoufox({
21
+ headless: false,
22
+ user_data_dir: profileDir,
23
+ geoip: true,
24
+ humanize: true,
25
+ os: "macos",
26
+ block_webrtc: true,
27
+ window: [1440, 900],
28
+ })) as unknown as BrowserContext;
29
+
30
+ try {
31
+ const pages = context.pages();
32
+ const page = pages.length > 0 ? pages[0] : await context.newPage();
33
+
34
+ await page.goto("https://x.com/login", {
35
+ waitUntil: "domcontentloaded",
36
+ timeout: 30_000,
37
+ });
38
+
39
+ notify("Log in via the browser window. Waiting for auth cookies...");
40
+
41
+ const deadline = Date.now() + 300_000;
42
+ while (Date.now() < deadline) {
43
+ try {
44
+ const cookies = await context.cookies("https://x.com");
45
+ const authNames = ["auth_token", "ct0", "twid"];
46
+ const found = cookies.filter((c) => authNames.includes(c.name));
47
+ if (found.length >= 2) {
48
+ await page.waitForTimeout(3000);
49
+ await context.close();
50
+ return { success: true, message: `Twitter login successful! (${found.map((c) => c.name).join(", ")})` };
51
+ }
52
+ } catch {
53
+ return { success: false, message: "Twitter login cancelled (browser closed)." };
54
+ }
55
+ await page.waitForTimeout(2000);
56
+ }
57
+
58
+ await context.close();
59
+ return { success: false, message: "Twitter login timed out after 5 minutes." };
60
+ } catch (error) {
61
+ try { await context.close(); } catch { /* already closed */ }
62
+ const msg = error instanceof Error ? error.message : String(error);
63
+ return { success: false, message: `Twitter login failed: ${msg}` };
64
+ }
65
+ }
66
+
67
+ export function registerTwitterLoginTool(pi: ExtensionAPI): void {
68
+ pi.registerTool({
69
+ name: "trigger_twitter_login",
70
+ label: "Twitter Login",
71
+ description:
72
+ "Open a browser window for the user to log in to Twitter/X. Call this when get_twitter_sentiment reports that login is needed and the user has confirmed they want to proceed. Returns success/failure. After success, retry get_twitter_sentiment.",
73
+ promptSnippet:
74
+ "trigger_twitter_login: Opens a browser for Twitter/X login. Use when Twitter sentiment is unavailable due to missing session.",
75
+ parameters: Type.Object({}),
76
+
77
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
78
+ if (!ctx?.hasUI) {
79
+ return {
80
+ content: [{ type: "text", text: "Cannot open browser in non-interactive mode. Twitter login requires a terminal session with UI." }],
81
+ details: { success: false },
82
+ };
83
+ }
84
+
85
+ const result = await runTwitterLogin((msg) => ctx.ui.notify(msg, "info"));
86
+
87
+ return {
88
+ content: [{ type: "text", text: result.message }],
89
+ details: { success: result.success },
90
+ };
91
+ },
92
+ });
93
+ }
@@ -0,0 +1,41 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { getFearGreedIndex } from "../../providers/fear-greed.js";
4
+ import { wrapProvider } from "../../providers/wrap-provider.js";
5
+ import type { FearGreedData } from "../../types/sentiment.js";
6
+
7
+ const params = Type.Object({});
8
+
9
+ export const fearGreedTool: AgentTool<typeof params, FearGreedData> = {
10
+ name: "get_fear_greed",
11
+ label: "Fear & Greed Index",
12
+ description:
13
+ "Get the Crypto Fear & Greed Index (alternative.me) — a sentiment indicator from 0 (Extreme Fear) to 100 (Extreme Greed). Includes current value and previous close.",
14
+ parameters: params,
15
+ async execute(_toolCallId, _args) {
16
+ const result = await wrapProvider("feargreed", () => getFearGreedIndex());
17
+ if (result.status === "unavailable") {
18
+ return {
19
+ content: [{ type: "text", text: `⚠ Fear & Greed Index unavailable (${result.reason}).` }],
20
+ details: null as any,
21
+ };
22
+ }
23
+ const fg = result.data;
24
+
25
+ const gauge = buildGauge(fg.value);
26
+ const text = [
27
+ `**Fear & Greed Index: ${fg.value} — ${fg.label}**`,
28
+ gauge,
29
+ `Previous Close: ${fg.previousClose}${fg.weekAgo != null ? ` | Week Ago: ${fg.weekAgo}` : ""}${fg.monthAgo != null ? ` | Month Ago: ${fg.monthAgo}` : ""}`,
30
+ ].join("\n");
31
+
32
+ return { content: [{ type: "text", text }], details: fg };
33
+ },
34
+ };
35
+
36
+ function buildGauge(value: number): string {
37
+ const width = 20;
38
+ const pos = Math.round((value / 100) * width);
39
+ const bar = "█".repeat(pos) + "░".repeat(width - pos);
40
+ return `[${bar}] ${value}/100`;
41
+ }
@@ -0,0 +1,80 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { getSeries } from "../../providers/fred.js";
4
+ import { wrapProvider } from "../../providers/wrap-provider.js";
5
+ import { getConfig } from "../../config.js";
6
+ import { withCredentialCheck } from "../../onboarding/tool-helpers.js";
7
+ import { FRED_SERIES } from "../../types/macro.js";
8
+ import type { FredSeries } from "../../types/macro.js";
9
+
10
+ const params = Type.Object({
11
+ series_id: Type.String({
12
+ description: `FRED series ID. Common ones: ${Object.entries(FRED_SERIES).map(([k, v]) => `${v} (${k})`).join(", ")}`,
13
+ }),
14
+ limit: Type.Optional(
15
+ Type.Number({ description: "Number of observations to return. Default: 30" }),
16
+ ),
17
+ });
18
+
19
+ export const fredDataTool: AgentTool<typeof params, FredSeries | { credentialRequired: unknown }> = {
20
+ name: "get_economic_data",
21
+ label: "FRED Economic Data",
22
+ description:
23
+ "Get economic data from FRED (Federal Reserve Economic Data): interest rates, CPI, GDP, unemployment, yield curve, and more. Requires FRED.",
24
+ parameters: params,
25
+ async execute(_toolCallId, args) {
26
+ return withCredentialCheck("fred", async () => {
27
+ const apiKey = getConfig().fredApiKey!;
28
+ const seriesId = args.series_id.toUpperCase();
29
+ const limit = normalizeLimit(seriesId, args.limit);
30
+ const result = await wrapProvider("fred", () => getSeries(seriesId, apiKey, limit));
31
+ if (result.status === "unavailable") {
32
+ return {
33
+ content: [{ type: "text", text: `⚠ FRED data unavailable for ${seriesId} (${result.reason}).` }],
34
+ details: null as any,
35
+ };
36
+ }
37
+ const series = result.data;
38
+
39
+ const latest = series.observations[series.observations.length - 1];
40
+ const header = `**${series.title}** (${series.id})`;
41
+ const meta = `Units: ${series.units} | Frequency: ${series.frequency} | Last updated: ${series.lastUpdated}`;
42
+ const current = latest ? `Latest: ${latest.value} (${latest.date})` : "No data";
43
+ const derived = formatDerivedChange(series);
44
+
45
+ // Show last 10 observations
46
+ const recent = series.observations.slice(-10);
47
+ const table = recent.map((o) => `${o.date}: ${o.value}`).join("\n");
48
+
49
+ const text = [header, meta, current, derived, "", "Recent observations:", table]
50
+ .filter(Boolean)
51
+ .join("\n");
52
+ const prefix = result.stale
53
+ ? `⚠ Using cached FRED data from ${result.timestamp} (FRED unavailable)\n`
54
+ : "";
55
+ return { content: [{ type: "text", text: prefix + text }], details: series };
56
+ });
57
+ },
58
+ };
59
+
60
+ function normalizeLimit(seriesId: string, requested: number | undefined): number {
61
+ const limit = requested ?? 30;
62
+ if (seriesId === FRED_SERIES.CPI && limit < 13) return 13;
63
+ return limit;
64
+ }
65
+
66
+ function formatDerivedChange(series: FredSeries): string | null {
67
+ const latest = series.observations.at(-1);
68
+ if (!latest) return null;
69
+ if (!/monthly/i.test(series.frequency) || !/\bindex\b/i.test(series.units)) return null;
70
+
71
+ const latestYear = Number(latest.date.slice(0, 4));
72
+ const monthDay = latest.date.slice(4);
73
+ const prior = series.observations.find((observation) =>
74
+ observation.date === `${latestYear - 1}${monthDay}`
75
+ );
76
+ if (!prior || prior.value === 0) return null;
77
+
78
+ const pct = ((latest.value / prior.value) - 1) * 100;
79
+ return `Derived YoY change: ${pct.toFixed(2)}% (${prior.date} to ${latest.date}).`;
80
+ }
@@ -0,0 +1,67 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { getCryptoHistory } from "../../providers/coingecko.js";
4
+ import { wrapProvider } from "../../providers/wrap-provider.js";
5
+ import type { OHLCV } from "../../types/market.js";
6
+ import { computeRiskMetrics } from "../portfolio/risk-analysis.js";
7
+
8
+ const params = Type.Object({
9
+ id: Type.String({
10
+ description: "CoinGecko coin ID (e.g. bitcoin, ethereum, solana)",
11
+ }),
12
+ days: Type.Optional(
13
+ Type.Number({
14
+ description: "Number of days of history: 1, 7, 14, 30, 90, 180, 365, max. Default: 180",
15
+ }),
16
+ ),
17
+ });
18
+
19
+ export const cryptoHistoryTool: AgentTool<typeof params, OHLCV[]> = {
20
+ name: "get_crypto_history",
21
+ label: "Crypto History",
22
+ description: "Get historical OHLC data for a cryptocurrency",
23
+ parameters: params,
24
+ async execute(_toolCallId, args) {
25
+ const id = args.id.toLowerCase();
26
+ const days = args.days ?? 180;
27
+ const result = await wrapProvider("coingecko", () => getCryptoHistory(id, days));
28
+ if (result.status === "unavailable") {
29
+ return {
30
+ content: [{ type: "text", text: `⚠ Crypto history unavailable for ${id} (${result.reason}).` }],
31
+ details: [],
32
+ };
33
+ }
34
+ const bars = result.data;
35
+
36
+ const summary = [
37
+ `${id} — ${bars.length} bars (${days} days)`,
38
+ `Period: ${bars[0]?.date} to ${bars[bars.length - 1]?.date}`,
39
+ ];
40
+
41
+ const recent = bars.slice(-10);
42
+ const table = recent
43
+ .map(
44
+ (b) =>
45
+ `${b.date} | O:${b.open.toFixed(2)} H:${b.high.toFixed(2)} L:${b.low.toFixed(2)} C:${b.close.toFixed(2)}`,
46
+ )
47
+ .join("\n");
48
+
49
+ const riskLines = buildRiskLines(id, bars);
50
+ const text = [...summary, ...riskLines, "", "Recent bars:", table].join("\n");
51
+ return { content: [{ type: "text", text }], details: bars };
52
+ },
53
+ };
54
+
55
+ function buildRiskLines(id: string, bars: OHLCV[]): string[] {
56
+ if (bars.length < 30) return [];
57
+ const metrics = computeRiskMetrics(id.toUpperCase(), bars.map((bar) => bar.close));
58
+ return [
59
+ "",
60
+ "Risk metrics from crypto history:",
61
+ `Annualized Return: ${(metrics.annualizedReturn * 100).toFixed(2)}%`,
62
+ `Annualized Volatility: ${(metrics.annualizedVolatility * 100).toFixed(2)}%`,
63
+ `Sharpe Ratio: ${metrics.sharpeRatio.toFixed(2)}`,
64
+ `Max Drawdown: ${(metrics.maxDrawdown * 100).toFixed(2)}%`,
65
+ `Value at Risk (95%, daily): ${(metrics.var95 * 100).toFixed(2)}%`,
66
+ ];
67
+ }
@@ -0,0 +1,53 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { getCryptoPrice } from "../../providers/coingecko.js";
4
+ import { wrapProvider } from "../../providers/wrap-provider.js";
5
+ import type { CryptoPrice } from "../../types/market.js";
6
+
7
+ const params = Type.Object({
8
+ id: Type.String({
9
+ description:
10
+ "CoinGecko coin ID (e.g. bitcoin, ethereum, solana, dogecoin). Use lowercase.",
11
+ }),
12
+ });
13
+
14
+ export const cryptoPriceTool: AgentTool<typeof params, CryptoPrice> = {
15
+ name: "get_crypto_price",
16
+ label: "Crypto Price",
17
+ description:
18
+ "Get current crypto price, 24h change, market cap, volume, ATH, and supply data",
19
+ parameters: params,
20
+ async execute(_toolCallId, args) {
21
+ const result = await wrapProvider("coingecko", () => getCryptoPrice(args.id.toLowerCase()));
22
+ if (result.status === "unavailable") {
23
+ return {
24
+ content: [{ type: "text", text: `⚠ Crypto price unavailable for ${args.id} (${result.reason}).` }],
25
+ details: null as any,
26
+ };
27
+ }
28
+ const crypto = result.data;
29
+ const sign = crypto.changePercent24h >= 0 ? "+" : "";
30
+ const text = [
31
+ `${crypto.name} (${crypto.symbol.toUpperCase()}): $${formatPrice(crypto.price)} (${sign}${crypto.changePercent24h.toFixed(2)}%)`,
32
+ `24h High: $${formatPrice(crypto.high24h)} | 24h Low: $${formatPrice(crypto.low24h)}`,
33
+ `Market Cap: $${formatLargeNumber(crypto.marketCap)} | 24h Volume: $${formatLargeNumber(crypto.volume24h)}`,
34
+ `ATH: $${formatPrice(crypto.ath)} (${crypto.athDate.split("T")[0]})`,
35
+ `Circulating: ${formatLargeNumber(crypto.circulatingSupply)} ${crypto.symbol.toUpperCase()}`,
36
+ ].join("\n");
37
+
38
+ return { content: [{ type: "text", text }], details: crypto };
39
+ },
40
+ };
41
+
42
+ function formatPrice(n: number): string {
43
+ if (n >= 1) return n.toFixed(2);
44
+ if (n >= 0.01) return n.toFixed(4);
45
+ return n.toFixed(8);
46
+ }
47
+
48
+ function formatLargeNumber(n: number): string {
49
+ if (n >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
50
+ if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
51
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
52
+ return n.toLocaleString();
53
+ }
@@ -0,0 +1,53 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { httpGet } from "../../infra/http-client.js";
4
+
5
+ const params = Type.Object({
6
+ query: Type.String({
7
+ description: "Search query — company name, ticker symbol, or crypto name (e.g. 'apple', 'AAPL', 'ethereum', 'bitcoin')",
8
+ }),
9
+ });
10
+
11
+ interface YahooSearchResponse {
12
+ quotes: Array<{
13
+ symbol: string;
14
+ shortname?: string;
15
+ longname?: string;
16
+ quoteType: string;
17
+ exchange: string;
18
+ score: number;
19
+ }>;
20
+ }
21
+
22
+ export const searchTickerTool: AgentTool<typeof params> = {
23
+ name: "search_ticker",
24
+ label: "Search Ticker",
25
+ description:
26
+ "Search for any ticker symbol — stocks, crypto, ETFs, indices, forex. Returns matching symbols with names and exchange info. Use this when you don't know the exact ticker for an asset.",
27
+ parameters: params,
28
+ async execute(_toolCallId, args) {
29
+ const url = `https://query1.finance.yahoo.com/v1/finance/search?q=${encodeURIComponent(args.query)}&quotesCount=10&newsCount=0`;
30
+ const data = await httpGet<YahooSearchResponse>(url, {
31
+ headers: { "User-Agent": "OpenCandle/1.0" },
32
+ });
33
+
34
+ const quotes = data.quotes ?? [];
35
+ if (quotes.length === 0) {
36
+ return {
37
+ content: [{ type: "text", text: `No results found for "${args.query}"` }],
38
+ details: quotes,
39
+ };
40
+ }
41
+
42
+ const lines = [
43
+ `**Search results for "${args.query}"** — ${quotes.length} matches`,
44
+ "",
45
+ ...quotes.map(
46
+ (q) =>
47
+ ` ${q.symbol} — ${q.longname || q.shortname || "N/A"} (${q.quoteType}, ${q.exchange})`,
48
+ ),
49
+ ];
50
+
51
+ return { content: [{ type: "text", text: lines.join("\n") }], details: quotes };
52
+ },
53
+ };
@@ -0,0 +1,79 @@
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 { getDailyHistory } from "../../providers/alpha-vantage.js";
5
+ import { wrapProvider } from "../../providers/wrap-provider.js";
6
+ import { withFallback } from "../../providers/with-fallback.js";
7
+ import { getConfig } from "../../config.js";
8
+ import type { OHLCV } from "../../types/market.js";
9
+ import type { ProviderResult } from "../../runtime/evidence.js";
10
+
11
+ const DAILY_INTERVALS = new Set(["1d", "1wk", "1mo"]);
12
+
13
+ const params = Type.Object({
14
+ symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT)" }),
15
+ range: Type.Optional(
16
+ Type.String({
17
+ description: "Time range: 1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, max. Default: 6mo",
18
+ }),
19
+ ),
20
+ interval: Type.Optional(
21
+ Type.String({
22
+ description: "Data interval: 1m, 5m, 15m, 1h, 1d, 1wk, 1mo. Default: 1d",
23
+ }),
24
+ ),
25
+ });
26
+
27
+ export const stockHistoryTool: AgentTool<typeof params, OHLCV[]> = {
28
+ name: "get_stock_history",
29
+ label: "Stock History",
30
+ description: "Get historical OHLCV (open, high, low, close, volume) data for a stock",
31
+ parameters: params,
32
+ async execute(_toolCallId, args) {
33
+ const symbol = args.symbol.toUpperCase();
34
+ const range = args.range ?? "6mo";
35
+ const interval = args.interval ?? "1d";
36
+ const apiKey = getConfig().alphaVantageApiKey;
37
+
38
+ let result: ProviderResult<OHLCV[]>;
39
+
40
+ if (DAILY_INTERVALS.has(interval) && apiKey) {
41
+ // Daily or above — can fall back to Alpha Vantage
42
+ result = await withFallback([
43
+ { provider: "yahoo", fn: () => getHistory(symbol, range, interval) },
44
+ { provider: "alphavantage", fn: () => getDailyHistory(symbol, apiKey, range) },
45
+ ]);
46
+ } else {
47
+ // Intraday — no cross-provider fallback
48
+ result = await wrapProvider("yahoo", () => getHistory(symbol, range, interval));
49
+ }
50
+
51
+ if (result.status === "unavailable") {
52
+ const intradayNote = !DAILY_INTERVALS.has(interval)
53
+ ? ` No alternate source for ${interval} data.`
54
+ : "";
55
+ return {
56
+ content: [{ type: "text", text: `⚠ Stock history unavailable for ${symbol} (${result.reason}).${intradayNote}` }],
57
+ details: [],
58
+ };
59
+ }
60
+ const bars = result.data;
61
+
62
+ const summary = [
63
+ `${symbol} — ${bars.length} bars (${range}, ${interval})`,
64
+ `Period: ${bars[0]?.date} to ${bars[bars.length - 1]?.date}`,
65
+ ];
66
+
67
+ // Include last 10 bars as sample
68
+ const recent = bars.slice(-10);
69
+ const table = recent
70
+ .map(
71
+ (b) =>
72
+ `${b.date} | O:${b.open.toFixed(2)} H:${b.high.toFixed(2)} L:${b.low.toFixed(2)} C:${b.close.toFixed(2)} V:${b.volume.toLocaleString()}`,
73
+ )
74
+ .join("\n");
75
+
76
+ const text = [...summary, "", "Recent bars:", table].join("\n");
77
+ return { content: [{ type: "text", text }], details: bars };
78
+ },
79
+ };
@@ -0,0 +1,64 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { getQuote } from "../../providers/yahoo-finance.js";
4
+ import { getGlobalQuote } from "../../providers/alpha-vantage.js";
5
+ import { withFallback } from "../../providers/with-fallback.js";
6
+ import { getConfig } from "../../config.js";
7
+ import type { StockQuote } from "../../types/market.js";
8
+
9
+ const params = Type.Object({
10
+ symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT, TSLA)" }),
11
+ });
12
+
13
+ export const stockQuoteTool: AgentTool<typeof params, StockQuote> = {
14
+ name: "get_stock_quote",
15
+ label: "Stock Quote",
16
+ description:
17
+ "Get real-time stock price, volume, market cap, and 52-week range for a ticker symbol",
18
+ parameters: params,
19
+ async execute(_toolCallId, args) {
20
+ const symbol = args.symbol.toUpperCase();
21
+ const apiKey = getConfig().alphaVantageApiKey;
22
+
23
+ const entries = [
24
+ { provider: "yahoo" as const, fn: () => getQuote(symbol) },
25
+ ...(apiKey
26
+ ? [{ provider: "alphavantage" as const, fn: () => getGlobalQuote(symbol, apiKey) }]
27
+ : []),
28
+ ];
29
+
30
+ const result = await withFallback(entries);
31
+ if (result.status === "unavailable") {
32
+ return {
33
+ content: [{ type: "text", text: `⚠ Stock quote unavailable for ${symbol} (${result.reason}).` }],
34
+ details: null as any,
35
+ };
36
+ }
37
+ const quote = result.data;
38
+ const sign = quote.changePercent >= 0 ? "+" : "";
39
+
40
+ const week52 = quote.week52High > 0 && quote.week52Low > 0
41
+ ? `$${quote.week52Low.toFixed(2)} - $${quote.week52High.toFixed(2)}`
42
+ : "N/A";
43
+ const marketCapStr = quote.marketCap > 0 ? `$${formatLargeNumber(quote.marketCap)}` : "N/A";
44
+
45
+ const text = [
46
+ `${quote.symbol}: $${quote.price.toFixed(2)} (${sign}${quote.changePercent.toFixed(2)}%)`,
47
+ `Open: $${quote.open.toFixed(2)} | High: $${quote.high.toFixed(2)} | Low: $${quote.low.toFixed(2)}`,
48
+ `Volume: ${quote.volume.toLocaleString()} | Market Cap: ${marketCapStr}`,
49
+ `52W Range: ${week52}`,
50
+ ].join("\n");
51
+
52
+ const prefix = result.stale
53
+ ? `⚠ Using cached quote from ${result.timestamp} (provider rate limited)\n`
54
+ : "";
55
+ return { content: [{ type: "text", text: prefix + text }], details: quote };
56
+ },
57
+ };
58
+
59
+ function formatLargeNumber(n: number): string {
60
+ if (n >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
61
+ if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
62
+ if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
63
+ return n.toLocaleString();
64
+ }
@@ -0,0 +1,81 @@
1
+ import type { Greeks } from "../../types/options.js";
2
+
3
+ interface GreeksInput {
4
+ type: "call" | "put";
5
+ spot: number; // underlying price
6
+ strike: number; // strike price
7
+ timeYears: number; // time to expiration in years
8
+ iv: number; // implied volatility (e.g. 0.30 for 30%)
9
+ riskFreeRate: number; // risk-free rate (e.g. 0.05 for 5%)
10
+ }
11
+
12
+ /**
13
+ * Compute option Greeks using the Black-Scholes model.
14
+ */
15
+ export function computeGreeks(input: GreeksInput): Greeks {
16
+ const { type, spot, strike, timeYears, iv, riskFreeRate: r } = input;
17
+
18
+ // At expiration: return intrinsic values
19
+ if (timeYears <= 0) {
20
+ const isCall = type === "call";
21
+ const itm = isCall ? spot >= strike : spot <= strike;
22
+ return {
23
+ delta: itm ? (isCall ? 1 : -1) : 0,
24
+ gamma: 0,
25
+ theta: 0,
26
+ vega: 0,
27
+ rho: 0,
28
+ };
29
+ }
30
+
31
+ const sqrtT = Math.sqrt(timeYears);
32
+ const d1 = (Math.log(spot / strike) + (r + (iv * iv) / 2) * timeYears) / (iv * sqrtT);
33
+ const d2 = d1 - iv * sqrtT;
34
+
35
+ const nd1 = cdf(d1);
36
+ const nd2 = cdf(d2);
37
+ const npd1 = pdf(d1);
38
+
39
+ const expRT = Math.exp(-r * timeYears);
40
+
41
+ if (type === "call") {
42
+ return {
43
+ delta: nd1,
44
+ gamma: npd1 / (spot * iv * sqrtT),
45
+ theta: (-(spot * npd1 * iv) / (2 * sqrtT) - r * strike * expRT * nd2) / 365,
46
+ vega: (spot * npd1 * sqrtT) / 100, // per 1% change in IV
47
+ rho: (strike * timeYears * expRT * nd2) / 100, // per 1% change in rate
48
+ };
49
+ } else {
50
+ const nMinusD2 = cdf(-d2);
51
+ return {
52
+ delta: nd1 - 1,
53
+ gamma: npd1 / (spot * iv * sqrtT),
54
+ theta: (-(spot * npd1 * iv) / (2 * sqrtT) + r * strike * expRT * nMinusD2) / 365,
55
+ vega: (spot * npd1 * sqrtT) / 100,
56
+ rho: -(strike * timeYears * expRT * nMinusD2) / 100,
57
+ };
58
+ }
59
+ }
60
+
61
+ /** Standard normal cumulative distribution function */
62
+ function cdf(x: number): number {
63
+ // Abramowitz and Stegun approximation 26.2.17
64
+ const a1 = 0.254829592;
65
+ const a2 = -0.284496736;
66
+ const a3 = 1.421413741;
67
+ const a4 = -1.453152027;
68
+ const a5 = 1.061405429;
69
+ const p = 0.3275911;
70
+
71
+ const sign = x < 0 ? -1 : 1;
72
+ const absX = Math.abs(x);
73
+ const t = 1 / (1 + p * absX);
74
+ const y = 1 - ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-absX * absX / 2);
75
+ return 0.5 * (1 + sign * y);
76
+ }
77
+
78
+ /** Standard normal probability density function */
79
+ function pdf(x: number): number {
80
+ return Math.exp(-0.5 * x * x) / Math.sqrt(2 * Math.PI);
81
+ }