opencandle 0.3.0 → 0.4.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 (283) hide show
  1. package/assets/logo.svg +187 -0
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +38 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +9 -0
  6. package/dist/config.js +13 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/infra/browser.d.ts +10 -0
  9. package/dist/infra/browser.js +1 -0
  10. package/dist/infra/browser.js.map +1 -1
  11. package/dist/infra/native-dependencies.d.ts +1 -0
  12. package/dist/infra/native-dependencies.js +10 -0
  13. package/dist/infra/native-dependencies.js.map +1 -0
  14. package/dist/infra/node-version.d.ts +2 -0
  15. package/dist/infra/node-version.js +23 -0
  16. package/dist/infra/node-version.js.map +1 -0
  17. package/dist/memory/index.d.ts +2 -0
  18. package/dist/memory/index.js +1 -0
  19. package/dist/memory/index.js.map +1 -1
  20. package/dist/memory/sqlite.js +42 -4
  21. package/dist/memory/sqlite.js.map +1 -1
  22. package/dist/memory/storage.d.ts +6 -0
  23. package/dist/memory/storage.js +3 -3
  24. package/dist/memory/storage.js.map +1 -1
  25. package/dist/memory/tool-defaults.d.ts +8 -0
  26. package/dist/memory/tool-defaults.js +59 -0
  27. package/dist/memory/tool-defaults.js.map +1 -0
  28. package/dist/onboarding/connect.d.ts +13 -1
  29. package/dist/onboarding/connect.js +21 -10
  30. package/dist/onboarding/connect.js.map +1 -1
  31. package/dist/onboarding/prompt-user.d.ts +1 -1
  32. package/dist/onboarding/providers.d.ts +7 -0
  33. package/dist/onboarding/providers.js +6 -3
  34. package/dist/onboarding/providers.js.map +1 -1
  35. package/dist/onboarding/tool-helpers.d.ts +1 -1
  36. package/dist/pi/opencandle-extension.d.ts +7 -1
  37. package/dist/pi/opencandle-extension.js +186 -10
  38. package/dist/pi/opencandle-extension.js.map +1 -1
  39. package/dist/pi/session-storage.d.ts +2 -0
  40. package/dist/pi/session-storage.js +5 -0
  41. package/dist/pi/session-storage.js.map +1 -0
  42. package/dist/pi/session.d.ts +4 -1
  43. package/dist/pi/session.js +25 -3
  44. package/dist/pi/session.js.map +1 -1
  45. package/dist/pi/setup.d.ts +1 -1
  46. package/dist/pi/setup.js +1 -1
  47. package/dist/pi/setup.js.map +1 -1
  48. package/dist/pi/tool-adapter.d.ts +2 -2
  49. package/dist/pi/tool-adapter.js +14 -1
  50. package/dist/pi/tool-adapter.js.map +1 -1
  51. package/dist/prompts/context-builder.d.ts +22 -0
  52. package/dist/prompts/context-builder.js +45 -10
  53. package/dist/prompts/context-builder.js.map +1 -1
  54. package/dist/prompts/disclaimer.d.ts +6 -0
  55. package/dist/prompts/disclaimer.js +9 -0
  56. package/dist/prompts/disclaimer.js.map +1 -0
  57. package/dist/prompts/workflow-prompts.d.ts +8 -0
  58. package/dist/prompts/workflow-prompts.js +39 -5
  59. package/dist/prompts/workflow-prompts.js.map +1 -1
  60. package/dist/providers/yahoo-finance.js +70 -33
  61. package/dist/providers/yahoo-finance.js.map +1 -1
  62. package/dist/routing/defaults.js +1 -1
  63. package/dist/routing/defaults.js.map +1 -1
  64. package/dist/routing/index.d.ts +4 -0
  65. package/dist/routing/index.js +3 -0
  66. package/dist/routing/index.js.map +1 -1
  67. package/dist/routing/router-llm-client.d.ts +11 -0
  68. package/dist/routing/router-llm-client.js +42 -0
  69. package/dist/routing/router-llm-client.js.map +1 -0
  70. package/dist/routing/router-prompt.d.ts +2 -0
  71. package/dist/routing/router-prompt.js +138 -0
  72. package/dist/routing/router-prompt.js.map +1 -0
  73. package/dist/routing/router-types.d.ts +62 -0
  74. package/dist/routing/router-types.js +2 -0
  75. package/dist/routing/router-types.js.map +1 -0
  76. package/dist/routing/router.d.ts +10 -0
  77. package/dist/routing/router.js +194 -0
  78. package/dist/routing/router.js.map +1 -0
  79. package/dist/runtime/session-coordinator.d.ts +63 -3
  80. package/dist/runtime/session-coordinator.js +155 -4
  81. package/dist/runtime/session-coordinator.js.map +1 -1
  82. package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
  83. package/dist/runtime/tool-defaults-wrapper.js +25 -0
  84. package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
  85. package/dist/sentiment/store.js +5 -0
  86. package/dist/sentiment/store.js.map +1 -1
  87. package/dist/system-prompt.js +20 -12
  88. package/dist/system-prompt.js.map +1 -1
  89. package/dist/tool-kit.d.ts +4 -4
  90. package/dist/tools/fundamentals/company-overview.d.ts +1 -1
  91. package/dist/tools/fundamentals/comps.d.ts +1 -1
  92. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  93. package/dist/tools/fundamentals/earnings.d.ts +1 -1
  94. package/dist/tools/fundamentals/financials.d.ts +1 -1
  95. package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
  96. package/dist/tools/index.d.ts +28 -1
  97. package/dist/tools/index.js +27 -0
  98. package/dist/tools/index.js.map +1 -1
  99. package/dist/tools/interaction/ask-user.d.ts +1 -1
  100. package/dist/tools/interaction/twitter-login.d.ts +1 -1
  101. package/dist/tools/macro/fear-greed.d.ts +1 -1
  102. package/dist/tools/macro/fred-data.d.ts +1 -1
  103. package/dist/tools/market/crypto-history.d.ts +1 -1
  104. package/dist/tools/market/crypto-price.d.ts +1 -1
  105. package/dist/tools/market/search-ticker.d.ts +1 -1
  106. package/dist/tools/market/stock-history.d.ts +1 -1
  107. package/dist/tools/market/stock-quote.d.ts +1 -1
  108. package/dist/tools/options/option-chain.d.ts +1 -1
  109. package/dist/tools/options/option-chain.js +4 -1
  110. package/dist/tools/options/option-chain.js.map +1 -1
  111. package/dist/tools/portfolio/correlation.d.ts +1 -1
  112. package/dist/tools/portfolio/predictions.d.ts +1 -1
  113. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  114. package/dist/tools/portfolio/tracker.d.ts +1 -1
  115. package/dist/tools/portfolio/watchlist.d.ts +1 -1
  116. package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
  117. package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
  118. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  119. package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
  120. package/dist/tools/sentiment/web-search.d.ts +1 -1
  121. package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
  122. package/dist/tools/technical/backtest.d.ts +1 -1
  123. package/dist/tools/technical/indicators.d.ts +1 -1
  124. package/dist/tools/technical/indicators.js +7 -1
  125. package/dist/tools/technical/indicators.js.map +1 -1
  126. package/dist/workflows/options-screener.js +7 -2
  127. package/dist/workflows/options-screener.js.map +1 -1
  128. package/dist/workflows/portfolio-builder.js +3 -3
  129. package/dist/workflows/portfolio-builder.js.map +1 -1
  130. package/gui/server/background-quotes.ts +31 -0
  131. package/gui/server/chat-event-adapter.ts +142 -0
  132. package/gui/server/invoke-tool.ts +89 -0
  133. package/gui/server/live-chat-event-adapter.ts +181 -0
  134. package/gui/server/model-setup.ts +100 -0
  135. package/gui/server/package.json +5 -0
  136. package/gui/server/projector.ts +212 -0
  137. package/gui/server/server.ts +592 -0
  138. package/gui/server/session-actions.ts +31 -0
  139. package/gui/server/tool-metadata.ts +88 -0
  140. package/gui/server/websocket.ts +128 -0
  141. package/gui/server/writer-lock.ts +118 -0
  142. package/gui/shared/chat-events.ts +118 -0
  143. package/gui/shared/event-reducer.ts +186 -0
  144. package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
  145. package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
  146. package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
  147. package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
  148. package/gui/web/dist/index.html +17 -0
  149. package/package.json +44 -18
  150. package/src/analysts/contracts.ts +189 -0
  151. package/src/analysts/orchestrator.ts +300 -0
  152. package/src/cli.ts +205 -0
  153. package/src/config.ts +161 -0
  154. package/src/index.ts +5 -0
  155. package/src/infra/browser.ts +111 -0
  156. package/src/infra/cache.ts +103 -0
  157. package/src/infra/http-client.ts +68 -0
  158. package/src/infra/index.ts +18 -0
  159. package/src/infra/native-dependencies.ts +12 -0
  160. package/src/infra/node-version.ts +24 -0
  161. package/src/infra/open-url.ts +28 -0
  162. package/src/infra/opencandle-paths.ts +64 -0
  163. package/src/infra/rate-limiter.ts +64 -0
  164. package/src/memory/index.ts +10 -0
  165. package/src/memory/manager.ts +159 -0
  166. package/src/memory/preference-extractor.ts +106 -0
  167. package/src/memory/retrieval.ts +70 -0
  168. package/src/memory/sqlite.ts +172 -0
  169. package/src/memory/storage.ts +204 -0
  170. package/src/memory/tool-defaults.ts +87 -0
  171. package/src/memory/types.ts +67 -0
  172. package/src/onboarding/connect.ts +184 -0
  173. package/src/onboarding/credential-interceptor.ts +134 -0
  174. package/src/onboarding/degradation-accumulator.ts +79 -0
  175. package/src/onboarding/prompt-user.ts +85 -0
  176. package/src/onboarding/providers.ts +315 -0
  177. package/src/onboarding/state.ts +218 -0
  178. package/src/onboarding/tool-helpers.ts +111 -0
  179. package/src/onboarding/tool-tags.ts +201 -0
  180. package/src/onboarding/validation.ts +158 -0
  181. package/src/pi/opencandle-extension.ts +724 -0
  182. package/src/pi/session-storage.ts +5 -0
  183. package/src/pi/session.ts +81 -0
  184. package/src/pi/setup.ts +371 -0
  185. package/src/pi/tool-adapter.ts +36 -0
  186. package/src/prompts/context-builder.ts +204 -0
  187. package/src/prompts/disclaimer.ts +9 -0
  188. package/src/prompts/sections.ts +46 -0
  189. package/src/prompts/workflow-prompts.ts +279 -0
  190. package/src/providers/alpha-vantage.ts +292 -0
  191. package/src/providers/coingecko.ts +96 -0
  192. package/src/providers/exa-search.ts +373 -0
  193. package/src/providers/fear-greed.ts +45 -0
  194. package/src/providers/finnhub.ts +124 -0
  195. package/src/providers/fred.ts +83 -0
  196. package/src/providers/index.ts +9 -0
  197. package/src/providers/provider-credential-error.ts +23 -0
  198. package/src/providers/reddit.ts +151 -0
  199. package/src/providers/sec-edgar.ts +96 -0
  200. package/src/providers/twitter.ts +173 -0
  201. package/src/providers/web-search.ts +293 -0
  202. package/src/providers/with-fallback.ts +41 -0
  203. package/src/providers/wrap-provider.ts +64 -0
  204. package/src/providers/yahoo-finance.ts +367 -0
  205. package/src/routing/classify-intent.ts +194 -0
  206. package/src/routing/defaults.ts +29 -0
  207. package/src/routing/entity-extractor.ts +140 -0
  208. package/src/routing/index.ts +26 -0
  209. package/src/routing/router-llm-client.ts +51 -0
  210. package/src/routing/router-prompt.ts +159 -0
  211. package/src/routing/router-types.ts +66 -0
  212. package/src/routing/router.ts +213 -0
  213. package/src/routing/slot-resolver.ts +152 -0
  214. package/src/routing/types.ts +63 -0
  215. package/src/runtime/evidence.ts +77 -0
  216. package/src/runtime/index.ts +55 -0
  217. package/src/runtime/prompt-step.ts +75 -0
  218. package/src/runtime/provider-ids.ts +15 -0
  219. package/src/runtime/provider-tracker.ts +40 -0
  220. package/src/runtime/run-context.ts +22 -0
  221. package/src/runtime/session-coordinator.ts +406 -0
  222. package/src/runtime/tool-defaults-wrapper.ts +35 -0
  223. package/src/runtime/validation.ts +214 -0
  224. package/src/runtime/workflow-events.ts +75 -0
  225. package/src/runtime/workflow-runner.ts +188 -0
  226. package/src/runtime/workflow-types.ts +102 -0
  227. package/src/sentiment/adapters/finnhub.ts +44 -0
  228. package/src/sentiment/adapters/reddit.ts +65 -0
  229. package/src/sentiment/adapters/twitter.ts +36 -0
  230. package/src/sentiment/adapters/web.ts +44 -0
  231. package/src/sentiment/index.ts +58 -0
  232. package/src/sentiment/keywords.ts +9 -0
  233. package/src/sentiment/pipeline.ts +68 -0
  234. package/src/sentiment/scorer.ts +78 -0
  235. package/src/sentiment/store.ts +260 -0
  236. package/src/sentiment/trends.ts +90 -0
  237. package/src/sentiment/types.ts +108 -0
  238. package/src/system-prompt.ts +115 -0
  239. package/src/tool-kit.ts +68 -0
  240. package/src/tools/AGENTS.md +36 -0
  241. package/src/tools/fundamentals/company-overview.ts +54 -0
  242. package/src/tools/fundamentals/comps.ts +156 -0
  243. package/src/tools/fundamentals/dcf.ts +267 -0
  244. package/src/tools/fundamentals/earnings.ts +47 -0
  245. package/src/tools/fundamentals/financials.ts +54 -0
  246. package/src/tools/fundamentals/sec-filings.ts +61 -0
  247. package/src/tools/index.ts +88 -0
  248. package/src/tools/interaction/ask-user.ts +81 -0
  249. package/src/tools/interaction/twitter-login.ts +93 -0
  250. package/src/tools/macro/fear-greed.ts +41 -0
  251. package/src/tools/macro/fred-data.ts +54 -0
  252. package/src/tools/market/crypto-history.ts +51 -0
  253. package/src/tools/market/crypto-price.ts +53 -0
  254. package/src/tools/market/search-ticker.ts +53 -0
  255. package/src/tools/market/stock-history.ts +79 -0
  256. package/src/tools/market/stock-quote.ts +64 -0
  257. package/src/tools/options/greeks.ts +82 -0
  258. package/src/tools/options/option-chain.ts +91 -0
  259. package/src/tools/portfolio/correlation.ts +162 -0
  260. package/src/tools/portfolio/predictions.ts +253 -0
  261. package/src/tools/portfolio/risk-analysis.ts +134 -0
  262. package/src/tools/portfolio/tracker.ts +147 -0
  263. package/src/tools/portfolio/watchlist.ts +153 -0
  264. package/src/tools/sentiment/reddit-sentiment.ts +164 -0
  265. package/src/tools/sentiment/sentiment-summary.ts +256 -0
  266. package/src/tools/sentiment/sentiment-trend.ts +58 -0
  267. package/src/tools/sentiment/twitter-sentiment.ts +96 -0
  268. package/src/tools/sentiment/web-search.ts +150 -0
  269. package/src/tools/sentiment/web-sentiment.ts +76 -0
  270. package/src/tools/technical/backtest.ts +246 -0
  271. package/src/tools/technical/indicators.ts +258 -0
  272. package/src/types/fundamentals.ts +46 -0
  273. package/src/types/index.ts +20 -0
  274. package/src/types/macro.ts +27 -0
  275. package/src/types/market.ts +43 -0
  276. package/src/types/options.ts +35 -0
  277. package/src/types/portfolio.ts +41 -0
  278. package/src/types/sentiment.ts +70 -0
  279. package/src/workflows/compare-assets.ts +39 -0
  280. package/src/workflows/index.ts +4 -0
  281. package/src/workflows/options-screener.ts +49 -0
  282. package/src/workflows/portfolio-builder.ts +52 -0
  283. package/src/workflows/types.ts +4 -0
@@ -0,0 +1,258 @@
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 { OHLCV } from "../../types/market.js";
6
+
7
+ // --- Volume-based indicators ---
8
+
9
+ export function computeOBV(bars: OHLCV[]): number[] {
10
+ const obv: number[] = [0];
11
+ for (let i = 1; i < bars.length; i++) {
12
+ if (bars[i].close > bars[i - 1].close) {
13
+ obv.push(obv[i - 1] + bars[i].volume);
14
+ } else if (bars[i].close < bars[i - 1].close) {
15
+ obv.push(obv[i - 1] - bars[i].volume);
16
+ } else {
17
+ obv.push(obv[i - 1]);
18
+ }
19
+ }
20
+ return obv;
21
+ }
22
+
23
+ export function computeVWAP(bars: OHLCV[]): number[] {
24
+ const vwap: number[] = [];
25
+ let cumPV = 0;
26
+ let cumVol = 0;
27
+ for (const bar of bars) {
28
+ const tp = (bar.high + bar.low + bar.close) / 3;
29
+ cumPV += tp * bar.volume;
30
+ cumVol += bar.volume;
31
+ vwap.push(cumVol === 0 ? 0 : cumPV / cumVol);
32
+ }
33
+ return vwap;
34
+ }
35
+
36
+ const params = Type.Object({
37
+ symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT)" }),
38
+ range: Type.Optional(
39
+ Type.String({ description: "Time range for data: 3mo, 6mo, 1y, 2y. Default: 1y" }),
40
+ ),
41
+ });
42
+
43
+ export const technicalIndicatorsTool: AgentTool<typeof params> = {
44
+ name: "get_technical_indicators",
45
+ label: "Technical Indicators",
46
+ description:
47
+ "Compute technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands) from historical price data. All computed locally — no API dependency.",
48
+ parameters: params,
49
+ async execute(toolCallId, args) {
50
+ const symbol = args.symbol.toUpperCase();
51
+ const range = args.range ?? "1y";
52
+ const result = await wrapProvider("yahoo", () => getHistory(symbol, range, "1d"));
53
+ if (result.status === "unavailable") {
54
+ return {
55
+ content: [{ type: "text", text: `⚠ Technical indicators unavailable for ${symbol} (${result.reason}).` }],
56
+ details: null as any,
57
+ };
58
+ }
59
+ const bars = result.data;
60
+ const closes = bars.map((b) => b.close);
61
+
62
+ if (closes.length < 26) {
63
+ return {
64
+ content: [{ type: "text", text: `Insufficient data for ${symbol} (need 26+ bars, got ${closes.length})` }],
65
+ details: null,
66
+ };
67
+ }
68
+
69
+ const sma20 = computeSMA(closes, 20);
70
+ const sma50 = computeSMA(closes, 50);
71
+ const ema12 = computeEMA(closes, 12);
72
+ const ema26 = computeEMA(closes, 26);
73
+ const rsi = computeRSI(closes, 14);
74
+ const macd = computeMACD(closes);
75
+ const bb = computeBollingerBands(closes, 20, 2);
76
+ const obv = computeOBV(bars);
77
+ const vwap = computeVWAP(bars);
78
+
79
+ const latest = closes[closes.length - 1];
80
+ const latestRsi = rsi[rsi.length - 1];
81
+ const latestMacd = macd[macd.length - 1];
82
+ const latestBB = bb[bb.length - 1];
83
+ const latestVwap = vwap[vwap.length - 1];
84
+ const obvTrend = obv.length >= 20
85
+ ? (obv[obv.length - 1] > obv[obv.length - 20] ? "Rising" : "Falling")
86
+ : "N/A";
87
+
88
+ const lines = [
89
+ `**${symbol} Technical Analysis** (${bars[0].date} to ${bars[bars.length - 1].date})`,
90
+ `Price: $${latest.toFixed(2)}`,
91
+ ``,
92
+ `SMA(20): $${sma20[sma20.length - 1]?.toFixed(2) ?? "N/A"} | SMA(50): $${sma50[sma50.length - 1]?.toFixed(2) ?? "N/A"}`,
93
+ `RSI(14): ${latestRsi?.toFixed(1) ?? "N/A"} ${rsiSignal(latestRsi)}`,
94
+ `MACD: ${latestMacd?.macd.toFixed(2) ?? "N/A"} | Signal: ${latestMacd?.signal.toFixed(2) ?? "N/A"} | Histogram: ${latestMacd?.histogram.toFixed(2) ?? "N/A"}`,
95
+ `Bollinger Bands: Upper $${latestBB?.upper.toFixed(2) ?? "N/A"} | Mid $${latestBB?.middle.toFixed(2) ?? "N/A"} | Lower $${latestBB?.lower.toFixed(2) ?? "N/A"}`,
96
+ `OBV Trend: ${obvTrend} | VWAP (cumulative): $${latestVwap?.toFixed(2) ?? "N/A"}`,
97
+ ``,
98
+ trendSummary(latest, sma20, sma50, latestRsi, latestMacd, obvTrend, latestVwap),
99
+ ];
100
+
101
+ return {
102
+ content: [{ type: "text", text: lines.join("\n") }],
103
+ details: {
104
+ symbol,
105
+ range,
106
+ prices: closes,
107
+ dates: bars.map((b) => b.date),
108
+ sma20, sma50, rsi, macd, bb, obv, vwap,
109
+ },
110
+ };
111
+ },
112
+ };
113
+
114
+ // --- Indicator computations (all pure functions) ---
115
+
116
+ export function computeSMA(data: number[], period: number): number[] {
117
+ const result: number[] = [];
118
+ for (let i = period - 1; i < data.length; i++) {
119
+ let sum = 0;
120
+ for (let j = i - period + 1; j <= i; j++) sum += data[j];
121
+ result.push(sum / period);
122
+ }
123
+ return result;
124
+ }
125
+
126
+ export function computeEMA(data: number[], period: number): number[] {
127
+ const k = 2 / (period + 1);
128
+ const result: number[] = [];
129
+ // Seed with SMA of first `period` values
130
+ let sum = 0;
131
+ for (let i = 0; i < period; i++) sum += data[i];
132
+ let ema = sum / period;
133
+ result.push(ema);
134
+ for (let i = period; i < data.length; i++) {
135
+ ema = data[i] * k + ema * (1 - k);
136
+ result.push(ema);
137
+ }
138
+ return result;
139
+ }
140
+
141
+ export function computeRSI(data: number[], period: number = 14): number[] {
142
+ const result: number[] = [];
143
+ const gains: number[] = [];
144
+ const losses: number[] = [];
145
+
146
+ for (let i = 1; i < data.length; i++) {
147
+ const diff = data[i] - data[i - 1];
148
+ gains.push(diff > 0 ? diff : 0);
149
+ losses.push(diff < 0 ? -diff : 0);
150
+ }
151
+
152
+ if (gains.length < period) return result;
153
+
154
+ let avgGain = gains.slice(0, period).reduce((a, b) => a + b, 0) / period;
155
+ let avgLoss = losses.slice(0, period).reduce((a, b) => a + b, 0) / period;
156
+
157
+ const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
158
+ result.push(100 - 100 / (1 + rs));
159
+
160
+ for (let i = period; i < gains.length; i++) {
161
+ avgGain = (avgGain * (period - 1) + gains[i]) / period;
162
+ avgLoss = (avgLoss * (period - 1) + losses[i]) / period;
163
+ const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
164
+ result.push(100 - 100 / (1 + rs));
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ export function computeMACD(
171
+ data: number[],
172
+ fastPeriod = 12,
173
+ slowPeriod = 26,
174
+ signalPeriod = 9,
175
+ ): { macd: number; signal: number; histogram: number }[] {
176
+ if (data.length < slowPeriod + signalPeriod) return [];
177
+
178
+ const emaFast = computeEMA(data, fastPeriod);
179
+ const emaSlow = computeEMA(data, slowPeriod);
180
+
181
+ // Align: emaFast starts at index fastPeriod-1, emaSlow at slowPeriod-1
182
+ const offset = slowPeriod - fastPeriod;
183
+ const macdLine: number[] = [];
184
+ for (let i = 0; i < emaSlow.length; i++) {
185
+ macdLine.push(emaFast[i + offset] - emaSlow[i]);
186
+ }
187
+
188
+ if (macdLine.length < signalPeriod) return [];
189
+
190
+ const signalLine = computeEMA(macdLine, signalPeriod);
191
+ const signalOffset = signalPeriod - 1;
192
+
193
+ const result: { macd: number; signal: number; histogram: number }[] = [];
194
+ for (let i = 0; i < signalLine.length; i++) {
195
+ const m = macdLine[i + signalOffset];
196
+ const s = signalLine[i];
197
+ result.push({ macd: m, signal: s, histogram: m - s });
198
+ }
199
+
200
+ return result;
201
+ }
202
+
203
+ export function computeBollingerBands(
204
+ data: number[],
205
+ period: number = 20,
206
+ stdDev: number = 2,
207
+ ): { upper: number; middle: number; lower: number }[] {
208
+ const result: { upper: number; middle: number; lower: number }[] = [];
209
+ for (let i = period - 1; i < data.length; i++) {
210
+ const slice = data.slice(i - period + 1, i + 1);
211
+ const mean = slice.reduce((a, b) => a + b, 0) / period;
212
+ const variance = slice.reduce((a, b) => a + (b - mean) ** 2, 0) / period;
213
+ const sd = Math.sqrt(variance);
214
+ result.push({
215
+ upper: mean + stdDev * sd,
216
+ middle: mean,
217
+ lower: mean - stdDev * sd,
218
+ });
219
+ }
220
+ return result;
221
+ }
222
+
223
+ function rsiSignal(rsi: number | undefined): string {
224
+ if (rsi == null) return "";
225
+ if (rsi >= 70) return "(Overbought)";
226
+ if (rsi <= 30) return "(Oversold)";
227
+ return "(Neutral)";
228
+ }
229
+
230
+ function trendSummary(
231
+ price: number,
232
+ sma20: number[],
233
+ sma50: number[],
234
+ rsi: number | undefined,
235
+ macd: { macd: number; signal: number; histogram: number } | undefined,
236
+ obvTrend?: string,
237
+ vwap?: number,
238
+ ): string {
239
+ const signals: string[] = [];
240
+
241
+ const latestSma20 = sma20[sma20.length - 1];
242
+ const latestSma50 = sma50[sma50.length - 1];
243
+
244
+ if (latestSma20 && price > latestSma20) signals.push("Price above SMA(20) — short-term bullish");
245
+ if (latestSma20 && price < latestSma20) signals.push("Price below SMA(20) — short-term bearish");
246
+ if (latestSma20 && latestSma50 && latestSma20 > latestSma50) signals.push("Golden cross pattern (SMA20 > SMA50)");
247
+ if (latestSma20 && latestSma50 && latestSma20 < latestSma50) signals.push("Death cross pattern (SMA20 < SMA50)");
248
+ if (rsi != null && rsi >= 70) signals.push("RSI overbought — potential reversal");
249
+ if (rsi != null && rsi <= 30) signals.push("RSI oversold — potential bounce");
250
+ if (macd && macd.histogram > 0) signals.push("MACD bullish (histogram positive)");
251
+ if (macd && macd.histogram < 0) signals.push("MACD bearish (histogram negative)");
252
+ if (obvTrend === "Rising" && price > (latestSma20 ?? 0)) signals.push("Volume confirming price advance (OBV rising)");
253
+ if (obvTrend === "Falling" && price < (latestSma20 ?? Infinity)) signals.push("Volume confirming price decline (OBV falling)");
254
+ if (vwap != null && price > vwap) signals.push("Price above cumulative VWAP — bullish volume-weighted bias");
255
+ if (vwap != null && price < vwap) signals.push("Price below cumulative VWAP — bearish volume-weighted bias");
256
+
257
+ return signals.length > 0 ? "Signals: " + signals.join(" | ") : "No strong signals";
258
+ }
@@ -0,0 +1,46 @@
1
+ export interface CompanyOverview {
2
+ symbol: string;
3
+ name: string;
4
+ description: string;
5
+ exchange: string;
6
+ sector: string;
7
+ industry: string;
8
+ marketCap: number;
9
+ pe: number | null;
10
+ forwardPe: number | null;
11
+ eps: number | null;
12
+ dividendYield: number | null;
13
+ beta: number | null;
14
+ week52High: number;
15
+ week52Low: number;
16
+ avgVolume: number;
17
+ profitMargin: number | null;
18
+ revenueGrowth: number | null;
19
+ }
20
+
21
+ export interface EarningsData {
22
+ symbol: string;
23
+ quarterly: Array<{
24
+ date: string;
25
+ reportedEPS: number;
26
+ estimatedEPS: number;
27
+ surprise: number;
28
+ surprisePercent: number;
29
+ }>;
30
+ }
31
+
32
+ export interface FinancialStatement {
33
+ fiscalDate: string;
34
+ revenue: number;
35
+ grossProfit: number;
36
+ operatingIncome: number;
37
+ netIncome: number;
38
+ eps: number;
39
+ totalAssets: number;
40
+ totalLiabilities: number;
41
+ totalEquity: number;
42
+ operatingCashFlow: number;
43
+ freeCashFlow: number;
44
+ totalDebt?: number;
45
+ cashAndEquivalents?: number;
46
+ }
@@ -0,0 +1,20 @@
1
+ export type { StockQuote, OHLCV, CryptoPrice } from "./market.js";
2
+ export type { CompanyOverview, EarningsData, FinancialStatement } from "./fundamentals.js";
3
+ export type { FredObservation, FredSeries } from "./macro.js";
4
+ export { FRED_SERIES } from "./macro.js";
5
+ export type { Greeks, OptionContract, OptionsChain } from "./options.js";
6
+ export type { Position, PortfolioSummary, RiskMetrics, TechnicalIndicators } from "./portfolio.js";
7
+ export type { FearGreedData, RedditSentimentResult, WebSearchResult, WebSearchEnvelope } from "./sentiment.js";
8
+
9
+ /**
10
+ * Handler for `ask_user` tool invocations in non-UI contexts (e.g. test harness).
11
+ * When provided to `createOpenCandleSession`, the ask-user tool calls this handler
12
+ * instead of `ctx.ui.*` methods.
13
+ */
14
+ export type AskUserHandler = (params: {
15
+ question: string;
16
+ questionType: "select" | "text" | "confirm";
17
+ options?: string[];
18
+ placeholder?: string;
19
+ reason?: string;
20
+ }) => Promise<{ answer: string | null; cancelled: boolean }>;
@@ -0,0 +1,27 @@
1
+ export interface FredObservation {
2
+ date: string;
3
+ value: number;
4
+ }
5
+
6
+ export interface FredSeries {
7
+ id: string;
8
+ title: string;
9
+ observations: FredObservation[];
10
+ units: string;
11
+ frequency: string;
12
+ lastUpdated: string;
13
+ }
14
+
15
+ // Common FRED series IDs
16
+ export const FRED_SERIES = {
17
+ FED_FUNDS: "FEDFUNDS",
18
+ TREASURY_10Y: "DGS10",
19
+ TREASURY_2Y: "DGS2",
20
+ TREASURY_30Y: "DGS30",
21
+ CPI: "CPIAUCSL",
22
+ UNEMPLOYMENT: "UNRATE",
23
+ GDP: "GDP",
24
+ YIELD_SPREAD_10Y2Y: "T10Y2Y",
25
+ INFLATION_EXPECTATION: "T5YIE",
26
+ MORTGAGE_30Y: "MORTGAGE30US",
27
+ } as const;
@@ -0,0 +1,43 @@
1
+ export interface StockQuote {
2
+ symbol: string;
3
+ price: number;
4
+ change: number;
5
+ changePercent: number;
6
+ open: number;
7
+ high: number;
8
+ low: number;
9
+ previousClose: number;
10
+ volume: number;
11
+ marketCap: number;
12
+ pe: number | null;
13
+ week52High: number;
14
+ week52Low: number;
15
+ timestamp: number;
16
+ }
17
+
18
+ export interface OHLCV {
19
+ date: string;
20
+ open: number;
21
+ high: number;
22
+ low: number;
23
+ close: number;
24
+ volume: number;
25
+ }
26
+
27
+ export interface CryptoPrice {
28
+ id: string;
29
+ symbol: string;
30
+ name: string;
31
+ price: number;
32
+ change24h: number;
33
+ changePercent24h: number;
34
+ marketCap: number;
35
+ volume24h: number;
36
+ high24h: number;
37
+ low24h: number;
38
+ ath: number;
39
+ athDate: string;
40
+ circulatingSupply: number;
41
+ totalSupply: number | null;
42
+ timestamp: number;
43
+ }
@@ -0,0 +1,35 @@
1
+ export interface Greeks {
2
+ delta: number;
3
+ gamma: number;
4
+ theta: number;
5
+ vega: number;
6
+ rho: number;
7
+ }
8
+
9
+ export interface OptionContract {
10
+ contractSymbol: string;
11
+ type: "call" | "put";
12
+ strike: number;
13
+ expiration: string;
14
+ bid: number;
15
+ ask: number;
16
+ lastPrice: number;
17
+ volume: number;
18
+ openInterest: number;
19
+ impliedVolatility: number;
20
+ inTheMoney: boolean;
21
+ greeks: Greeks;
22
+ }
23
+
24
+ export interface OptionsChain {
25
+ symbol: string;
26
+ underlyingPrice: number;
27
+ expirationDate: string;
28
+ expirationDates: string[];
29
+ calls: OptionContract[];
30
+ puts: OptionContract[];
31
+ totalCallVolume: number;
32
+ totalPutVolume: number;
33
+ putCallRatio: number;
34
+ fetchedAt: string;
35
+ }
@@ -0,0 +1,41 @@
1
+ export interface Position {
2
+ symbol: string;
3
+ shares: number;
4
+ avgCost: number;
5
+ addedAt: string;
6
+ }
7
+
8
+ export interface PortfolioSummary {
9
+ positions: Array<
10
+ Position & {
11
+ currentPrice: number;
12
+ marketValue: number;
13
+ totalCost: number;
14
+ pnl: number;
15
+ pnlPercent: number;
16
+ }
17
+ >;
18
+ totalValue: number;
19
+ totalCost: number;
20
+ totalPnl: number;
21
+ totalPnlPercent: number;
22
+ }
23
+
24
+ export interface RiskMetrics {
25
+ symbol: string;
26
+ annualizedReturn: number;
27
+ annualizedVolatility: number;
28
+ sharpeRatio: number;
29
+ maxDrawdown: number;
30
+ var95: number; // 95% Value at Risk (daily)
31
+ }
32
+
33
+ export interface TechnicalIndicators {
34
+ symbol: string;
35
+ period: string;
36
+ sma: number[];
37
+ ema: number[];
38
+ rsi: number[];
39
+ macd: { macd: number; signal: number; histogram: number }[];
40
+ bollingerBands: { upper: number; middle: number; lower: number }[];
41
+ }
@@ -0,0 +1,70 @@
1
+ export interface FearGreedData {
2
+ value: number;
3
+ label: string; // "Extreme Fear" | "Fear" | "Neutral" | "Greed" | "Extreme Greed"
4
+ timestamp: number;
5
+ previousClose: number;
6
+ weekAgo: number | null;
7
+ monthAgo: number | null;
8
+ }
9
+
10
+ export interface TwitterTweet {
11
+ id: string;
12
+ text: string;
13
+ author: string;
14
+ likes: number;
15
+ retweets: number;
16
+ replies: number;
17
+ views: number | null;
18
+ url: string;
19
+ created: string;
20
+ }
21
+
22
+ export interface TwitterSentimentResult {
23
+ query: string;
24
+ tweetCount: number;
25
+ tweets: TwitterTweet[];
26
+ sentimentScore: number; // -1.0 (fully bearish) to +1.0 (fully bullish)
27
+ bullishCount: number;
28
+ bearishCount: number;
29
+ topMentions: string[];
30
+ fetchedAt: string;
31
+ }
32
+
33
+ export interface WebSearchResult {
34
+ title: string;
35
+ url: string;
36
+ snippet: string;
37
+ /** Domain extracted from url (e.g., "reuters.com") */
38
+ source: string;
39
+ /** ISO 8601 timestamp, null if unknown */
40
+ published: string | null;
41
+ category: "news" | "general";
42
+ }
43
+
44
+ export interface WebSearchEnvelope {
45
+ query: string;
46
+ results: WebSearchResult[];
47
+ resultCount: number;
48
+ fetchedAt: string;
49
+ provider: "ddg" | "brave" | "exa";
50
+ }
51
+
52
+ export interface RedditSentimentResult {
53
+ subreddit: string;
54
+ postCount: number;
55
+ posts: Array<{
56
+ id: string;
57
+ title: string;
58
+ selftext: string;
59
+ author: string;
60
+ score: number;
61
+ comments: number;
62
+ url: string;
63
+ created: string;
64
+ }>;
65
+ topMentions: string[];
66
+ sentimentScore: number; // -1.0 (fully bearish) to +1.0 (fully bullish)
67
+ bullishCount: number;
68
+ bearishCount: number;
69
+ fetchedAt: string;
70
+ }
@@ -0,0 +1,39 @@
1
+ import type { CompareAssetsSlots, SlotResolution } from "../routing/types.js";
2
+ import { buildCompareAssetsPrompt } from "../prompts/workflow-prompts.js";
3
+ import type { WorkflowPlan } from "./types.js";
4
+ import type { WorkflowDefinition } from "../runtime/prompt-step.js";
5
+ import { promptStep } from "../runtime/prompt-step.js";
6
+
7
+ export function buildCompareAssetsWorkflowDefinition(
8
+ resolution: SlotResolution<CompareAssetsSlots>,
9
+ ): WorkflowDefinition {
10
+ const symbols = resolution.resolved.symbols.join(", ");
11
+
12
+ return {
13
+ workflowType: "compare_assets",
14
+ steps: [
15
+ promptStep("fetch_data", "Fetch data for all assets", buildCompareAssetsPrompt(resolution), {
16
+ requiredInputs: ["symbols"],
17
+ expectedOutputs: ["asset_data"],
18
+ }),
19
+ promptStep("compare_and_present", "Present side-by-side comparison", `Now present the side-by-side comparison for ${symbols}:
20
+ - Keep any unavailable fundamentals marked as unavailable instead of retrying the same failed provider calls.
21
+ - Use the price, technical, and risk data you already fetched to finish the comparison even if some fundamentals are missing.
22
+ - End with a concise verdict on which asset looks strongest right now and why.`, {
23
+ requiredInputs: ["asset_data"],
24
+ expectedOutputs: ["comparison_summary"],
25
+ }),
26
+ ],
27
+ };
28
+ }
29
+
30
+ /** @deprecated Use buildCompareAssetsWorkflowDefinition instead */
31
+ export function buildCompareAssetsWorkflow(
32
+ resolution: SlotResolution<CompareAssetsSlots>,
33
+ ): WorkflowPlan {
34
+ const def = buildCompareAssetsWorkflowDefinition(resolution);
35
+ return {
36
+ initialPrompt: def.steps[0].prompt,
37
+ followUps: def.steps.slice(1).map((s) => s.prompt),
38
+ };
39
+ }
@@ -0,0 +1,4 @@
1
+ export { buildPortfolioWorkflow, buildPortfolioWorkflowDefinition } from "./portfolio-builder.js";
2
+ export { buildCompareAssetsWorkflow, buildCompareAssetsWorkflowDefinition } from "./compare-assets.js";
3
+ export { buildOptionsScreenerWorkflow, buildOptionsScreenerWorkflowDefinition } from "./options-screener.js";
4
+ export type { WorkflowPlan } from "./types.js";
@@ -0,0 +1,49 @@
1
+ import type { OptionsScreenerSlots, SlotResolution } from "../routing/types.js";
2
+ import { buildOptionsScreenerPrompt } from "../prompts/workflow-prompts.js";
3
+ import type { WorkflowPlan } from "./types.js";
4
+ import type { WorkflowDefinition } from "../runtime/prompt-step.js";
5
+ import { promptStep } from "../runtime/prompt-step.js";
6
+
7
+ export function buildOptionsScreenerWorkflowDefinition(resolution: SlotResolution<OptionsScreenerSlots>): WorkflowDefinition {
8
+ const s = resolution.resolved;
9
+ const contractType = s.direction === "bullish" ? "calls" : "puts";
10
+
11
+ return {
12
+ workflowType: "options_screener",
13
+ steps: [
14
+ promptStep("fetch_chain", "Fetch option chain data", buildOptionsScreenerPrompt(resolution), {
15
+ requiredInputs: ["symbol"],
16
+ expectedOutputs: ["option_chain"],
17
+ }),
18
+ promptStep("rank_and_present", "Rank and present top contracts", `Now rank and present the top ${contractType} for ${s.symbol}. You MUST produce a final text response — never end this turn with only tool calls.
19
+
20
+ 1. From the option chain data already fetched, select the top 3-5 contracts matching: ${s.moneynessPreference} strikes, DTE near ${s.dteTarget}, with ${s.liquidityMinimum}.
21
+ 2. Rank by ${s.objective}: balance premium cost, delta exposure, and probability of profit. Only include contracts with |delta| >= 0.20.
22
+ 3. Present a table: strike, expiry, premium, delta, IV, open interest, bid-ask spread.
23
+ 4. Explain why the #1 pick is ranked highest.
24
+ 5. State all assumptions used (which were defaults vs user-specified vs saved preferences).
25
+ 6. Include risk caveats: max loss = premium, IV crush risk, time decay (theta).
26
+
27
+ If some or all of the option chain fetches returned "⚠ Options chain unavailable" or similar gaps, do NOT abort. Instead:
28
+ - Rank and present whatever contracts you did retrieve from the successful fetches, even if fewer than 3.
29
+ - If no chain data is usable at all, still produce a text response: reproduce the Assumptions block, state which expirations failed, and commit to a next step (e.g. "retry nearest monthly expiration", "use price/technicals instead"). Never end the turn with only tool calls.
30
+
31
+ Length constraints:
32
+ - Max 1 sentence explaining the #1 pick.
33
+ - Risk caveats: max 3 bullet points.
34
+ - Keep total response under 30 lines.`, {
35
+ requiredInputs: ["option_chain"],
36
+ expectedOutputs: ["ranked_contracts"],
37
+ }),
38
+ ],
39
+ };
40
+ }
41
+
42
+ /** @deprecated Use buildOptionsScreenerWorkflowDefinition instead */
43
+ export function buildOptionsScreenerWorkflow(resolution: SlotResolution<OptionsScreenerSlots>): WorkflowPlan {
44
+ const def = buildOptionsScreenerWorkflowDefinition(resolution);
45
+ return {
46
+ initialPrompt: def.steps[0].prompt,
47
+ followUps: def.steps.slice(1).map((s) => s.prompt),
48
+ };
49
+ }