opencandle 0.4.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 (251) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -14
  3. package/dist/cli.js +2 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +19 -3
  6. package/dist/config.js +61 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/infra/browser.d.ts +1 -3
  9. package/dist/infra/browser.js +1 -1
  10. package/dist/infra/browser.js.map +1 -1
  11. package/dist/infra/rate-limiter.d.ts +4 -0
  12. package/dist/infra/rate-limiter.js +5 -1
  13. package/dist/infra/rate-limiter.js.map +1 -1
  14. package/dist/memory/manager.d.ts +9 -0
  15. package/dist/memory/manager.js +28 -11
  16. package/dist/memory/manager.js.map +1 -1
  17. package/dist/memory/storage.d.ts +3 -2
  18. package/dist/memory/storage.js.map +1 -1
  19. package/dist/memory/types.js +4 -0
  20. package/dist/memory/types.js.map +1 -1
  21. package/dist/pi/opencandle-extension.js +230 -36
  22. package/dist/pi/opencandle-extension.js.map +1 -1
  23. package/dist/pi/setup.js +10 -0
  24. package/dist/pi/setup.js.map +1 -1
  25. package/dist/prompts/context-builder.d.ts +18 -3
  26. package/dist/prompts/context-builder.js +102 -16
  27. package/dist/prompts/context-builder.js.map +1 -1
  28. package/dist/prompts/disclaimer.js +1 -1
  29. package/dist/prompts/disclaimer.js.map +1 -1
  30. package/dist/prompts/policy-cards.d.ts +13 -0
  31. package/dist/prompts/policy-cards.js +197 -0
  32. package/dist/prompts/policy-cards.js.map +1 -0
  33. package/dist/prompts/sections.js +3 -3
  34. package/dist/prompts/sections.js.map +1 -1
  35. package/dist/prompts/workflow-prompts.js +170 -18
  36. package/dist/prompts/workflow-prompts.js.map +1 -1
  37. package/dist/providers/alpha-vantage.js +23 -1
  38. package/dist/providers/alpha-vantage.js.map +1 -1
  39. package/dist/providers/sec-edgar.d.ts +8 -1
  40. package/dist/providers/sec-edgar.js +172 -5
  41. package/dist/providers/sec-edgar.js.map +1 -1
  42. package/dist/providers/yahoo-finance.d.ts +2 -0
  43. package/dist/providers/yahoo-finance.js +134 -3
  44. package/dist/providers/yahoo-finance.js.map +1 -1
  45. package/dist/routing/classify-intent.d.ts +3 -0
  46. package/dist/routing/classify-intent.js +82 -3
  47. package/dist/routing/classify-intent.js.map +1 -1
  48. package/dist/routing/defaults.js +3 -3
  49. package/dist/routing/defaults.js.map +1 -1
  50. package/dist/routing/entity-extractor.d.ts +1 -0
  51. package/dist/routing/entity-extractor.js +158 -12
  52. package/dist/routing/entity-extractor.js.map +1 -1
  53. package/dist/routing/index.d.ts +7 -1
  54. package/dist/routing/index.js +4 -0
  55. package/dist/routing/index.js.map +1 -1
  56. package/dist/routing/legacy-rule-router.d.ts +9 -0
  57. package/dist/routing/legacy-rule-router.js +12 -0
  58. package/dist/routing/legacy-rule-router.js.map +1 -0
  59. package/dist/routing/planning.d.ts +54 -0
  60. package/dist/routing/planning.js +531 -0
  61. package/dist/routing/planning.js.map +1 -0
  62. package/dist/routing/route-manifest.d.ts +35 -0
  63. package/dist/routing/route-manifest.js +221 -0
  64. package/dist/routing/route-manifest.js.map +1 -0
  65. package/dist/routing/router-prompt.js +45 -42
  66. package/dist/routing/router-prompt.js.map +1 -1
  67. package/dist/routing/router-types.d.ts +9 -0
  68. package/dist/routing/router.d.ts +1 -0
  69. package/dist/routing/router.js +456 -12
  70. package/dist/routing/router.js.map +1 -1
  71. package/dist/routing/slot-resolver.js +46 -6
  72. package/dist/routing/slot-resolver.js.map +1 -1
  73. package/dist/routing/turn-context.d.ts +44 -0
  74. package/dist/routing/turn-context.js +45 -0
  75. package/dist/routing/turn-context.js.map +1 -0
  76. package/dist/routing/types.d.ts +13 -1
  77. package/dist/runtime/answer-contracts.d.ts +82 -0
  78. package/dist/runtime/answer-contracts.js +414 -0
  79. package/dist/runtime/answer-contracts.js.map +1 -0
  80. package/dist/runtime/artifact-contracts.d.ts +14 -0
  81. package/dist/runtime/artifact-contracts.js +57 -0
  82. package/dist/runtime/artifact-contracts.js.map +1 -0
  83. package/dist/runtime/planning-evidence.d.ts +99 -0
  84. package/dist/runtime/planning-evidence.js +445 -0
  85. package/dist/runtime/planning-evidence.js.map +1 -0
  86. package/dist/runtime/session-coordinator.d.ts +20 -2
  87. package/dist/runtime/session-coordinator.js +47 -14
  88. package/dist/runtime/session-coordinator.js.map +1 -1
  89. package/dist/system-prompt.js +4 -1
  90. package/dist/system-prompt.js.map +1 -1
  91. package/dist/tools/fundamentals/company-overview.js +1 -1
  92. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  93. package/dist/tools/fundamentals/comps.js +1 -1
  94. package/dist/tools/fundamentals/comps.js.map +1 -1
  95. package/dist/tools/fundamentals/dcf.js +1 -1
  96. package/dist/tools/fundamentals/dcf.js.map +1 -1
  97. package/dist/tools/fundamentals/earnings.js +1 -1
  98. package/dist/tools/fundamentals/earnings.js.map +1 -1
  99. package/dist/tools/fundamentals/financials.js +1 -1
  100. package/dist/tools/fundamentals/financials.js.map +1 -1
  101. package/dist/tools/fundamentals/sec-filings.d.ts +1 -0
  102. package/dist/tools/fundamentals/sec-filings.js +19 -2
  103. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  104. package/dist/tools/index.d.ts +1 -0
  105. package/dist/tools/index.js +3 -0
  106. package/dist/tools/index.js.map +1 -1
  107. package/dist/tools/macro/fear-greed.js +1 -1
  108. package/dist/tools/macro/fear-greed.js.map +1 -1
  109. package/dist/tools/macro/fred-data.js +29 -5
  110. package/dist/tools/macro/fred-data.js.map +1 -1
  111. package/dist/tools/market/crypto-history.js +18 -2
  112. package/dist/tools/market/crypto-history.js.map +1 -1
  113. package/dist/tools/market/crypto-price.js +1 -1
  114. package/dist/tools/market/crypto-price.js.map +1 -1
  115. package/dist/tools/market/search-ticker.js +1 -1
  116. package/dist/tools/market/search-ticker.js.map +1 -1
  117. package/dist/tools/market/stock-history.js +1 -1
  118. package/dist/tools/market/stock-history.js.map +1 -1
  119. package/dist/tools/market/stock-quote.js +1 -1
  120. package/dist/tools/market/stock-quote.js.map +1 -1
  121. package/dist/tools/options/greeks.js +0 -1
  122. package/dist/tools/options/greeks.js.map +1 -1
  123. package/dist/tools/options/option-chain.js +9 -4
  124. package/dist/tools/options/option-chain.js.map +1 -1
  125. package/dist/tools/portfolio/correlation.js +1 -1
  126. package/dist/tools/portfolio/correlation.js.map +1 -1
  127. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  128. package/dist/tools/portfolio/holdings-overlap.js +105 -0
  129. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  130. package/dist/tools/portfolio/predictions.js +1 -1
  131. package/dist/tools/portfolio/predictions.js.map +1 -1
  132. package/dist/tools/portfolio/risk-analysis.js +1 -1
  133. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  134. package/dist/tools/portfolio/tracker.js +1 -1
  135. package/dist/tools/portfolio/tracker.js.map +1 -1
  136. package/dist/tools/portfolio/watchlist.js +12 -4
  137. package/dist/tools/portfolio/watchlist.js.map +1 -1
  138. package/dist/tools/sentiment/reddit-sentiment.js +1 -1
  139. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  140. package/dist/tools/sentiment/sentiment-summary.js +57 -2
  141. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  142. package/dist/tools/sentiment/twitter-sentiment.js +1 -1
  143. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  144. package/dist/tools/sentiment/web-search.js +32 -3
  145. package/dist/tools/sentiment/web-search.js.map +1 -1
  146. package/dist/tools/sentiment/web-sentiment.js +1 -1
  147. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  148. package/dist/tools/technical/backtest.d.ts +2 -2
  149. package/dist/tools/technical/backtest.js +41 -27
  150. package/dist/tools/technical/backtest.js.map +1 -1
  151. package/dist/tools/technical/indicators.js +1 -3
  152. package/dist/tools/technical/indicators.js.map +1 -1
  153. package/dist/types/options.d.ts +10 -0
  154. package/dist/types/portfolio.d.ts +27 -0
  155. package/dist/workflows/compare-assets.js +38 -2
  156. package/dist/workflows/compare-assets.js.map +1 -1
  157. package/dist/workflows/options-screener.js +88 -7
  158. package/dist/workflows/options-screener.js.map +1 -1
  159. package/dist/workflows/portfolio-builder.js +7 -3
  160. package/dist/workflows/portfolio-builder.js.map +1 -1
  161. package/gui/server/ask-user-bridge.ts +82 -0
  162. package/gui/server/gui-session-manager.ts +5 -0
  163. package/gui/server/projector.ts +47 -5
  164. package/gui/server/prompt-observation.ts +61 -0
  165. package/gui/server/server.ts +119 -8
  166. package/gui/server/session-entry-wait.ts +81 -0
  167. package/gui/web/dist/assets/{CatalogOverlay-D1ImSJTe.js → CatalogOverlay-Bmp6Knu7.js} +1 -1
  168. package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
  169. package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
  170. package/gui/web/dist/index.html +2 -2
  171. package/package.json +18 -12
  172. package/src/cli.ts +2 -1
  173. package/src/config.ts +89 -5
  174. package/src/infra/browser.ts +1 -1
  175. package/src/infra/rate-limiter.ts +10 -1
  176. package/src/memory/manager.ts +43 -10
  177. package/src/memory/storage.ts +3 -2
  178. package/src/memory/types.ts +4 -0
  179. package/src/pi/opencandle-extension.ts +273 -42
  180. package/src/pi/setup.ts +10 -0
  181. package/src/prompts/context-builder.ts +128 -17
  182. package/src/prompts/disclaimer.ts +1 -1
  183. package/src/prompts/policy-cards.ts +220 -0
  184. package/src/prompts/sections.ts +3 -3
  185. package/src/prompts/workflow-prompts.ts +172 -18
  186. package/src/providers/alpha-vantage.ts +24 -1
  187. package/src/providers/sec-edgar.ts +220 -4
  188. package/src/providers/web-search.ts +1 -1
  189. package/src/providers/yahoo-finance.ts +171 -4
  190. package/src/routing/classify-intent.ts +94 -3
  191. package/src/routing/defaults.ts +3 -3
  192. package/src/routing/entity-extractor.ts +164 -13
  193. package/src/routing/index.ts +44 -0
  194. package/src/routing/legacy-rule-router.ts +13 -0
  195. package/src/routing/planning.ts +732 -0
  196. package/src/routing/route-manifest.ts +287 -0
  197. package/src/routing/router-prompt.ts +50 -46
  198. package/src/routing/router-types.ts +21 -0
  199. package/src/routing/router.ts +511 -12
  200. package/src/routing/slot-resolver.ts +44 -6
  201. package/src/routing/turn-context.ts +111 -0
  202. package/src/routing/types.ts +13 -1
  203. package/src/runtime/answer-contracts.ts +633 -0
  204. package/src/runtime/artifact-contracts.ts +76 -0
  205. package/src/runtime/planning-evidence.ts +591 -0
  206. package/src/runtime/session-coordinator.ts +78 -12
  207. package/src/system-prompt.ts +4 -1
  208. package/src/tools/fundamentals/company-overview.ts +1 -1
  209. package/src/tools/fundamentals/comps.ts +1 -1
  210. package/src/tools/fundamentals/dcf.ts +1 -1
  211. package/src/tools/fundamentals/earnings.ts +1 -1
  212. package/src/tools/fundamentals/financials.ts +1 -1
  213. package/src/tools/fundamentals/sec-filings.ts +25 -2
  214. package/src/tools/index.ts +3 -0
  215. package/src/tools/macro/fear-greed.ts +1 -1
  216. package/src/tools/macro/fred-data.ts +31 -5
  217. package/src/tools/market/crypto-history.ts +18 -2
  218. package/src/tools/market/crypto-price.ts +1 -1
  219. package/src/tools/market/search-ticker.ts +1 -1
  220. package/src/tools/market/stock-history.ts +1 -1
  221. package/src/tools/market/stock-quote.ts +1 -1
  222. package/src/tools/options/greeks.ts +0 -1
  223. package/src/tools/options/option-chain.ts +9 -4
  224. package/src/tools/portfolio/correlation.ts +1 -1
  225. package/src/tools/portfolio/holdings-overlap.ts +123 -0
  226. package/src/tools/portfolio/predictions.ts +1 -1
  227. package/src/tools/portfolio/risk-analysis.ts +1 -1
  228. package/src/tools/portfolio/tracker.ts +1 -1
  229. package/src/tools/portfolio/watchlist.ts +10 -4
  230. package/src/tools/sentiment/reddit-sentiment.ts +1 -1
  231. package/src/tools/sentiment/sentiment-summary.ts +62 -2
  232. package/src/tools/sentiment/twitter-sentiment.ts +1 -1
  233. package/src/tools/sentiment/web-search.ts +36 -3
  234. package/src/tools/sentiment/web-sentiment.ts +1 -1
  235. package/src/tools/technical/backtest.ts +50 -29
  236. package/src/tools/technical/indicators.ts +1 -3
  237. package/src/types/options.ts +17 -0
  238. package/src/types/portfolio.ts +32 -0
  239. package/src/workflows/compare-assets.ts +38 -2
  240. package/src/workflows/options-screener.ts +85 -7
  241. package/src/workflows/portfolio-builder.ts +7 -3
  242. package/dist/runtime/index.d.ts +0 -16
  243. package/dist/runtime/index.js +0 -10
  244. package/dist/runtime/index.js.map +0 -1
  245. package/dist/runtime/provider-ids.d.ts +0 -14
  246. package/dist/runtime/provider-ids.js +0 -14
  247. package/dist/runtime/provider-ids.js.map +0 -1
  248. package/gui/web/dist/assets/index-DBrWq43L.css +0 -1
  249. package/gui/web/dist/assets/index-RflHaj0y.js +0 -67
  250. package/src/runtime/index.ts +0 -55
  251. package/src/runtime/provider-ids.ts +0 -15
@@ -0,0 +1,123 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
+ import { getFundHoldings } from "../../providers/yahoo-finance.js";
4
+ import { wrapProvider } from "../../providers/wrap-provider.js";
5
+ import type {
6
+ FundHolding,
7
+ FundHoldings,
8
+ FundHoldingsOverlap,
9
+ FundOverlapPair,
10
+ SharedFundHolding,
11
+ } from "../../types/portfolio.js";
12
+
13
+ const params = Type.Object({
14
+ symbols: Type.Array(Type.String(), {
15
+ description: "Array of 2+ ETF or fund ticker symbols to compare holdings overlap, e.g. ['VOO','QQQ']",
16
+ minItems: 2,
17
+ }),
18
+ });
19
+
20
+ export const holdingsOverlapTool: AgentTool<typeof params, FundHoldingsOverlap | null> = {
21
+ name: "analyze_holdings_overlap",
22
+ label: "ETF Holdings Overlap",
23
+ description:
24
+ "Fetch top fund/ETF holdings and compute pairwise overlap by weight. Useful for ETF diversification and hidden concentration checks.",
25
+ parameters: params,
26
+ async execute(_toolCallId, args) {
27
+ const symbols = [...new Set(args.symbols.map((symbol) => symbol.toUpperCase()))];
28
+ if (symbols.length < 2) {
29
+ throw new Error("Need at least 2 symbols for holdings overlap analysis.");
30
+ }
31
+
32
+ const results = await Promise.all(symbols.map(async (symbol) => ({
33
+ symbol,
34
+ result: await wrapProvider("yahoo", () => getFundHoldings(symbol)),
35
+ })));
36
+ const unavailable = results.flatMap((entry) =>
37
+ entry.result.status === "unavailable"
38
+ ? [{ symbol: entry.symbol, reason: entry.result.reason }]
39
+ : []
40
+ );
41
+ if (unavailable.length > 0) {
42
+ const missing = unavailable.map((entry) => `${entry.symbol}: ${entry.reason}`).join("; ");
43
+ return {
44
+ content: [{ type: "text", text: `⚠ Holdings overlap unavailable for one or more funds (${missing}).` }],
45
+ details: null,
46
+ };
47
+ }
48
+
49
+ const funds = results.flatMap((entry) => entry.result.status === "ok" ? [entry.result.data] : []);
50
+ const overlap = computeHoldingsOverlap(funds);
51
+ return {
52
+ content: [{ type: "text", text: formatOverlap(overlap) }],
53
+ details: overlap,
54
+ };
55
+ },
56
+ };
57
+
58
+ export function computeHoldingsOverlap(funds: FundHoldings[]): FundHoldingsOverlap {
59
+ const pairs: FundOverlapPair[] = [];
60
+ for (let i = 0; i < funds.length; i += 1) {
61
+ for (let j = i + 1; j < funds.length; j += 1) {
62
+ pairs.push(computePairOverlap(funds[i], funds[j]));
63
+ }
64
+ }
65
+ return { funds, pairs };
66
+ }
67
+
68
+ function computePairOverlap(a: FundHoldings, b: FundHoldings): FundOverlapPair {
69
+ const aBySymbol = new Map(a.holdings.map((holding) => [holding.symbol, holding]));
70
+ const sharedHoldings: SharedFundHolding[] = [];
71
+ for (const bHolding of b.holdings) {
72
+ const aHolding = aBySymbol.get(bHolding.symbol);
73
+ if (!aHolding) continue;
74
+ const overlapWeight = roundWeight(Math.min(aHolding.weight, bHolding.weight));
75
+ sharedHoldings.push({
76
+ symbol: bHolding.symbol,
77
+ name: commonHoldingName(aHolding, bHolding),
78
+ weights: {
79
+ [a.symbol]: aHolding.weight,
80
+ [b.symbol]: bHolding.weight,
81
+ },
82
+ overlapWeight,
83
+ });
84
+ }
85
+ sharedHoldings.sort((left, right) => right.overlapWeight - left.overlapWeight);
86
+ return {
87
+ symbols: [a.symbol, b.symbol],
88
+ overlapWeight: roundWeight(sharedHoldings.reduce((sum, holding) => sum + holding.overlapWeight, 0)),
89
+ sharedHoldings,
90
+ };
91
+ }
92
+
93
+ function formatOverlap(overlap: FundHoldingsOverlap): string {
94
+ const lines: string[] = ["**ETF/Fund Holdings Overlap**", ""];
95
+ for (const pair of overlap.pairs) {
96
+ lines.push(`${pair.symbols.join("/")} holdings overlap: ${formatPercent(pair.overlapWeight)}`);
97
+ const topShared = pair.sharedHoldings.slice(0, 5);
98
+ if (topShared.length > 0) {
99
+ lines.push(`Top shared holdings: ${topShared.map((holding) =>
100
+ `${holding.symbol} (${formatPercent(holding.overlapWeight)} overlap)`
101
+ ).join(", ")}`);
102
+ } else {
103
+ lines.push("No shared top holdings found in provider coverage.");
104
+ }
105
+ lines.push("");
106
+ }
107
+ lines.push("Provider note: overlap uses provider top-holdings coverage, not full portfolio look-through unless the provider returns all holdings.");
108
+ return lines.join("\n").trimEnd();
109
+ }
110
+
111
+ function commonHoldingName(a: FundHolding, b: FundHolding): string {
112
+ if (a.name && a.name !== a.symbol) return a.name;
113
+ if (b.name && b.name !== b.symbol) return b.name;
114
+ return a.symbol;
115
+ }
116
+
117
+ function formatPercent(value: number): string {
118
+ return `${(value * 100).toFixed(1)}%`;
119
+ }
120
+
121
+ function roundWeight(value: number): number {
122
+ return Math.round(value * 10_000) / 10_000;
123
+ }
@@ -184,7 +184,7 @@ export const predictionsTool: AgentTool<typeof params> = {
184
184
  description:
185
185
  "Track your analysis predictions and measure accuracy over time. Record: save a directional prediction with conviction. Check: evaluate all predictions against current prices, compute hit rate and conviction-weighted accuracy. Inspired by ATLAS's Darwinian scoring approach.",
186
186
  parameters: params,
187
- async execute(toolCallId, args) {
187
+ async execute(_toolCallId, args) {
188
188
  if (args.action === "record") {
189
189
  if (!args.symbol || !args.direction || !args.conviction || !args.entry_price) {
190
190
  throw new Error("symbol, direction, conviction, and entry_price are required for record action.");
@@ -17,7 +17,7 @@ export const riskAnalysisTool: AgentTool<typeof params, RiskMetrics> = {
17
17
  description:
18
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
19
  parameters: params,
20
- async execute(toolCallId, args) {
20
+ async execute(_toolCallId, args) {
21
21
  const symbol = args.symbol.toUpperCase();
22
22
  const period = args.period ?? "1y";
23
23
  const result = await wrapProvider("yahoo", () => getHistory(symbol, period, "1d"));
@@ -51,7 +51,7 @@ export const portfolioTrackerTool: AgentTool<typeof params, PortfolioSummary | n
51
51
  description:
52
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
53
  parameters: params,
54
- async execute(toolCallId, args) {
54
+ async execute(_toolCallId, args) {
55
55
  const positions = loadPortfolio();
56
56
 
57
57
  if (args.action === "add") {
@@ -54,7 +54,7 @@ export const watchlistTool: AgentTool<typeof params> = {
54
54
  description:
55
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
56
  parameters: params,
57
- async execute(toolCallId, args) {
57
+ async execute(_toolCallId, args) {
58
58
  const items = loadWatchlist();
59
59
 
60
60
  if (args.action === "add") {
@@ -118,17 +118,22 @@ export const watchlistTool: AgentTool<typeof params> = {
118
118
  items.map(async (item) => {
119
119
  const result = await wrapProvider("yahoo", () => getQuote(item.symbol));
120
120
  if (result.status === "unavailable") {
121
- return { ...item, currentPrice: 0, alerts: [`UNAVAILABLE: ${result.reason}`] };
121
+ return { ...item, currentPrice: 0, alerts: [`UNAVAILABLE: ${result.reason}`], statuses: [] };
122
122
  }
123
123
  const quote = result.data;
124
124
  const alerts: string[] = [];
125
+ const statuses: string[] = [];
125
126
  if (item.targetPrice && quote.price >= item.targetPrice) {
126
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}`);
127
130
  }
128
131
  if (item.stopPrice && quote.price <= item.stopPrice) {
129
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}`);
130
135
  }
131
- return { ...item, currentPrice: quote.price, alerts };
136
+ return { ...item, currentPrice: quote.price, alerts, statuses };
132
137
  }),
133
138
  );
134
139
 
@@ -140,9 +145,10 @@ export const watchlistTool: AgentTool<typeof params> = {
140
145
 
141
146
  for (const c of checks) {
142
147
  const alertStr = c.alerts.length > 0 ? ` ** ${c.alerts.join(" | ")} **` : "";
148
+ const statusStr = c.statuses.length > 0 ? ` | ${c.statuses.join(" | ")}` : "";
143
149
  const targetStr = c.targetPrice ? ` | Target: $${c.targetPrice}` : "";
144
150
  const stopStr = c.stopPrice ? ` | Stop: $${c.stopPrice}` : "";
145
- lines.push(` ${c.symbol}: $${c.currentPrice.toFixed(2)}${targetStr}${stopStr}${alertStr}`);
151
+ lines.push(` ${c.symbol}: $${c.currentPrice.toFixed(2)}${targetStr}${stopStr}${statusStr}${alertStr}`);
146
152
  }
147
153
 
148
154
  return {
@@ -36,7 +36,7 @@ export const redditSentimentTool: AgentTool<typeof params, RedditSentimentResult
36
36
  description:
37
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
38
  parameters: params,
39
- async execute(toolCallId, args) {
39
+ async execute(_toolCallId, args) {
40
40
  const limit = Math.min(args.limit ?? 25, 100);
41
41
  const config = getConfig();
42
42
 
@@ -4,6 +4,7 @@ import { getSubredditPosts, getPostComments } from "../../providers/reddit.js";
4
4
  import { getTwitterSentiment } from "../../providers/twitter.js";
5
5
  import { searchWeb } from "../../providers/web-search.js";
6
6
  import { getCompanyNews, finnhubDateRange } from "../../providers/finnhub.js";
7
+ import { getQuote } from "../../providers/yahoo-finance.js";
7
8
  import { wrapProvider } from "../../providers/wrap-provider.js";
8
9
  import { getConfig } from "../../config.js";
9
10
  import { TwitterAdapter } from "../../sentiment/adapters/twitter.js";
@@ -28,14 +29,13 @@ export const sentimentSummaryTool: AgentTool<typeof params> = {
28
29
  description:
29
30
  "Cross-source sentiment summary combining Twitter, Reddit, and web/news. Returns per-source scores, aggregate sentiment, and divergence detection.",
30
31
  parameters: params,
31
- async execute(toolCallId, args) {
32
+ async execute(_toolCallId, args) {
32
33
  const hours = args.hours ?? 24;
33
34
  const config = getConfig();
34
35
  const warnings: string[] = [];
35
36
  const allRecords: SentinelRecord[] = [];
36
37
 
37
38
  const twitterAdapter = new TwitterAdapter();
38
- const redditAdapter = new RedditAdapter();
39
39
  const webAdapter = new WebAdapter();
40
40
  const finnhubAdapter = new FinnhubAdapter();
41
41
 
@@ -171,6 +171,15 @@ export const sentimentSummaryTool: AgentTool<typeof params> = {
171
171
  lines.push("");
172
172
  lines.push(`**Aggregate:** ${aggregate >= 0 ? "+" : ""}${aggregate.toFixed(2)} (${sentimentLabel(aggregate)})`);
173
173
 
174
+ const priceContext = await buildPriceContext(candidateTickers[0], aggregate);
175
+ if (priceContext) {
176
+ lines.push("");
177
+ lines.push(priceContext);
178
+ }
179
+
180
+ lines.push("");
181
+ lines.push("Source-coverage risk: sentiment can be noisy and missing sources can skew the signal; treat this as supporting evidence, not a standalone buy/sell input.");
182
+
174
183
  // Divergence
175
184
  if (result.divergence && result.divergence.detected) {
176
185
  lines.push("");
@@ -197,6 +206,57 @@ export const sentimentSummaryTool: AgentTool<typeof params> = {
197
206
  },
198
207
  };
199
208
 
209
+ async function buildPriceContext(symbol: string | undefined, aggregateSentiment: number): Promise<string | null> {
210
+ if (!symbol) return null;
211
+ try {
212
+ const quote = await getQuote(symbol);
213
+ const sign = quote.changePercent >= 0 ? "+" : "";
214
+ const direction = quote.changePercent > 0 ? "positive" : quote.changePercent < 0 ? "negative" : "flat";
215
+ const sentimentDirection = aggregateSentiment > 0 ? "positive" : aggregateSentiment < 0 ? "negative" : "neutral";
216
+ const relationship = sentimentDirection === "neutral" || direction === "flat" || sentimentDirection === direction
217
+ ? "roughly aligns with price action"
218
+ : "diverges from price action";
219
+ const freshnessNote = formatQuoteFreshnessNote(quote.timestamp);
220
+ return `Price context: ${quote.symbol}: $${quote.price.toFixed(2)} (${sign}${quote.changePercent.toFixed(2)}%).${freshnessNote} The ${sentimentDirection} sentiment signal ${relationship}.`;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+
226
+ function formatQuoteFreshnessNote(timestamp: number | undefined): string {
227
+ if (!timestamp) return "";
228
+ const quoteDate = new Date(timestamp);
229
+ if (Number.isNaN(quoteDate.getTime())) return "";
230
+
231
+ const now = new Date();
232
+ const quoteDay = quoteDate.toLocaleDateString("en-US", { timeZone: "America/New_York" });
233
+ const currentDay = now.toLocaleDateString("en-US", { timeZone: "America/New_York" });
234
+ const quoteStamp = quoteDate.toLocaleString("en-US", {
235
+ dateStyle: "medium",
236
+ timeStyle: "short",
237
+ timeZone: "America/New_York",
238
+ });
239
+
240
+ const weekday = new Intl.DateTimeFormat("en-US", {
241
+ weekday: "long",
242
+ timeZone: "America/New_York",
243
+ }).format(now);
244
+ const isWeekend = weekday === "Saturday" || weekday === "Sunday";
245
+
246
+ if (quoteDay === currentDay) {
247
+ const marketClosedNote = isWeekend
248
+ ? " U.S. markets are closed today, so treat this as delayed or last available price context, not active intraday trading."
249
+ : "";
250
+ return ` Quote timestamp: ${quoteStamp} ET.${marketClosedNote}`;
251
+ }
252
+
253
+ const marketClosedNote = isWeekend
254
+ ? " U.S. markets are closed today, so treat this as last trading-session price action."
255
+ : "";
256
+
257
+ return ` Last available quote timestamp: ${quoteStamp} ET.${marketClosedNote}`;
258
+ }
259
+
200
260
  async function fetchRedditCrossSubreddit(
201
261
  query: string,
202
262
  subreddits: string[],
@@ -24,7 +24,7 @@ export const twitterSentimentTool: AgentTool<typeof params, TwitterSentimentResu
24
24
  description:
25
25
  "Fetch recent tweets for a stock ticker or search query and compute engagement-weighted sentiment. Returns tweet data, sentiment score, and co-mentioned tickers. Requires a Twitter session via trigger_twitter_login.",
26
26
  parameters: params,
27
- async execute(toolCallId, args) {
27
+ async execute(_toolCallId, args) {
28
28
  const limit = Math.min(args.limit ?? 50, 200);
29
29
  const hours = args.hours ?? 24;
30
30
 
@@ -79,6 +79,33 @@ function buildSoftDegradedPrefix(data: WebSearchEnvelope): string {
79
79
  return tags.length === 0 ? "" : `${tags.join("\n")}\n\n`;
80
80
  }
81
81
 
82
+ function buildOfficialSourceGapPrefix(query: string, data: WebSearchEnvelope): string {
83
+ if (!hasOfficialFedSourceGap(query, data)) return "";
84
+
85
+ return [
86
+ "[OPENCANDLE_SOURCE_GAP source=fed_official evidence=missing remediation=\"verify against federalreserve.gov/FOMC before stating Fed announcements\"]",
87
+ "Hard source gap: no official Fed/FOMC source was returned. Do not present meeting announcements, votes, quotes, appointments, leadership changes, or named policy rationales as verified; treat results as market commentary only.",
88
+ "",
89
+ ].join("\n");
90
+ }
91
+
92
+ function hasOfficialFedSourceGap(query: string, data: WebSearchEnvelope): boolean {
93
+ return isFedAnnouncementQuery(query) &&
94
+ !data.results.some((result) => isOfficialFedSource(result.source) || isOfficialFedSource(result.url));
95
+ }
96
+
97
+ function isFedAnnouncementQuery(query: string): boolean {
98
+ const lower = query.toLowerCase();
99
+ const mentionsFed = /\b(?:fed|fomc|federal reserve)\b/.test(lower);
100
+ const asksOfficialFact = /\b(?:announcement|meeting|minutes|statement|decision|vote|chair|governor|appointment|leadership)\b/.test(lower);
101
+ return mentionsFed && asksOfficialFact;
102
+ }
103
+
104
+ function isOfficialFedSource(value: string): boolean {
105
+ const lower = value.toLowerCase();
106
+ return lower.includes("federalreserve.gov") || lower.includes("fomc.gov");
107
+ }
108
+
82
109
  export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
83
110
  name: "search_web",
84
111
  label: "Web Search",
@@ -87,7 +114,7 @@ export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
87
114
  "NOT for real-time prices, historical data, fundamentals, macro data, SEC filings, or social sentiment — those have dedicated tools.",
88
115
  parameters: params,
89
116
 
90
- async execute(toolCallId, args) {
117
+ async execute(_toolCallId, args) {
91
118
  const query = args.query?.trim();
92
119
  if (!query) {
93
120
  return {
@@ -114,11 +141,12 @@ export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
114
141
 
115
142
  if (data.resultCount === 0) {
116
143
  const zeroPrefix = buildSoftDegradedPrefix(data);
144
+ const sourceGapPrefix = buildOfficialSourceGapPrefix(query, data);
117
145
  return {
118
146
  content: [
119
147
  {
120
148
  type: "text",
121
- text: `${zeroPrefix}No results found for "${query}" (${category}, past ${freshness}).`,
149
+ text: `${zeroPrefix}${sourceGapPrefix}No results found for "${query}" (${category}, past ${freshness}).`,
122
150
  },
123
151
  ],
124
152
  details: data,
@@ -130,6 +158,8 @@ export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
130
158
  : "";
131
159
 
132
160
  const softDegradedPrefix = buildSoftDegradedPrefix(data);
161
+ const sourceGapPrefix = buildOfficialSourceGapPrefix(query, data);
162
+ const shouldOmitResults = hasOfficialFedSourceGap(query, data);
133
163
 
134
164
  const header = `**Web Search** — ${data.resultCount} results for "${query}" (${category}, past ${freshness}, via ${data.provider})`;
135
165
  const items = data.results.map((r) => {
@@ -139,8 +169,11 @@ export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
139
169
  const pub = r.published ? `Published: ${r.published}` : "Published: unknown";
140
170
  return `• [${title}](${url}) — ${r.source}\n ${snippet}\n ${pub}`;
141
171
  });
172
+ const body = shouldOmitResults
173
+ ? "Non-official results were omitted from assistant-visible evidence for this Fed/FOMC announcement query. Verify against an official Federal Reserve or FOMC source before naming announcements or personnel changes."
174
+ : items.join("\n\n");
142
175
 
143
- const text = `${softDegradedPrefix}${stalePrefix}${header}\n\n${items.join("\n\n")}`;
176
+ const text = `${softDegradedPrefix}${sourceGapPrefix}${stalePrefix}${header}\n\n${body}`;
144
177
 
145
178
  return {
146
179
  content: [{ type: "text", text }],
@@ -22,7 +22,7 @@ export const webSentimentTool: AgentTool<typeof params> = {
22
22
  description:
23
23
  "Analyze sentiment from web and news search results for a ticker or topic. Returns scored results with aggregate sentiment.",
24
24
  parameters: params,
25
- async execute(toolCallId, args) {
25
+ async execute(_toolCallId, args) {
26
26
  const freshness = args.freshness ?? "day";
27
27
  const limit = Math.min(args.limit ?? 10, 20);
28
28
 
@@ -5,7 +5,7 @@ import { wrapProvider } from "../../providers/wrap-provider.js";
5
5
  import { computeSMA, computeRSI } from "./indicators.js";
6
6
  import type { OHLCV } from "../../types/market.js";
7
7
 
8
- export type Strategy = "sma_crossover" | "rsi_mean_reversion";
8
+ export type Strategy = "sma_crossover" | "sma_50_200_crossover" | "rsi_mean_reversion";
9
9
 
10
10
  export interface BacktestResult {
11
11
  strategy: string;
@@ -20,28 +20,32 @@ export interface BacktestResult {
20
20
 
21
21
  export function runBacktest(bars: OHLCV[], strategy: Strategy): BacktestResult {
22
22
  const closes = bars.map((b) => b.close);
23
- const buyAndHoldReturn = closes.length > 1
24
- ? (closes[closes.length - 1] - closes[0]) / closes[0]
25
- : 0;
26
23
 
27
24
  if (strategy === "sma_crossover") {
28
- return backtestSMACrossover(bars, closes);
25
+ return backtestSMACrossover(bars, closes, 20, 50, strategy);
26
+ }
27
+ if (strategy === "sma_50_200_crossover") {
28
+ return backtestSMACrossover(bars, closes, 50, 200, strategy);
29
29
  }
30
30
  return backtestRSIMeanReversion(bars, closes);
31
31
  }
32
32
 
33
- function backtestSMACrossover(bars: OHLCV[], closes: number[]): BacktestResult {
34
- const sma20 = computeSMA(closes, 20);
35
- const sma50 = computeSMA(closes, 50);
33
+ function backtestSMACrossover(
34
+ bars: OHLCV[],
35
+ closes: number[],
36
+ shortWindow: number,
37
+ longWindow: number,
38
+ strategyName: Strategy,
39
+ ): BacktestResult {
40
+ const shortSma = computeSMA(closes, shortWindow);
41
+ const longSma = computeSMA(closes, longWindow);
36
42
 
37
- if (sma50.length === 0) {
38
- return emptyResult("sma_crossover", closes);
43
+ if (longSma.length === 0) {
44
+ return emptyResult(strategyName, closes);
39
45
  }
40
46
 
41
- // Align: SMA(20) starts at index 19, SMA(50) at index 49
42
- // sma20[i] corresponds to closes[i + 19], sma50[i] to closes[i + 49]
43
- const offset20 = 19;
44
- const offset50 = 49;
47
+ const shortOffset = shortWindow - 1;
48
+ const longOffset = longWindow - 1;
45
49
 
46
50
  let position = false;
47
51
  let entryPrice = 0;
@@ -50,19 +54,19 @@ function backtestSMACrossover(bars: OHLCV[], closes: number[]): BacktestResult {
50
54
  let peak = 1.0;
51
55
  let maxDd = 0;
52
56
 
53
- for (let i = 0; i < sma50.length; i++) {
54
- const barIdx = i + offset50;
55
- const sma20Idx = i + (offset50 - offset20);
56
- const s20 = sma20[sma20Idx];
57
- const s50 = sma50[i];
57
+ for (let i = 0; i < longSma.length; i++) {
58
+ const barIdx = i + longOffset;
59
+ const shortSmaIdx = i + (longOffset - shortOffset);
60
+ const sShort = shortSma[shortSmaIdx];
61
+ const sLong = longSma[i];
58
62
  const price = closes[barIdx];
59
63
 
60
- if (!position && s20 > s50) {
64
+ if (!position && sShort > sLong) {
61
65
  // Buy signal
62
66
  position = true;
63
67
  entryPrice = price;
64
68
  tradeLog.push({ type: "buy", date: bars[barIdx].date, price });
65
- } else if (position && s20 < s50) {
69
+ } else if (position && sShort < sLong) {
66
70
  // Sell signal
67
71
  const pnl = (price - entryPrice) / entryPrice;
68
72
  equity *= 1 + pnl;
@@ -87,7 +91,7 @@ function backtestSMACrossover(bars: OHLCV[], closes: number[]): BacktestResult {
87
91
  tradeLog.push({ type: "sell", date: bars[bars.length - 1].date, price: lastPrice, pnl });
88
92
  }
89
93
 
90
- return buildResult("sma_crossover", equity - 1, closes, tradeLog, maxDd);
94
+ return buildResult(strategyName, equity - 1, closes, tradeLog, maxDd);
91
95
  }
92
96
 
93
97
  function backtestRSIMeanReversion(bars: OHLCV[], closes: number[]): BacktestResult {
@@ -187,8 +191,8 @@ function emptyResult(strategy: string, closes: number[]): BacktestResult {
187
191
  const params = Type.Object({
188
192
  symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT, SPY)" }),
189
193
  strategy: Type.Union(
190
- [Type.Literal("sma_crossover"), Type.Literal("rsi_mean_reversion")],
191
- { description: "Strategy: sma_crossover (buy when SMA20 > SMA50, sell on reverse) or rsi_mean_reversion (buy when RSI < 30, sell when RSI > 70)" },
194
+ [Type.Literal("sma_crossover"), Type.Literal("sma_50_200_crossover"), Type.Literal("rsi_mean_reversion")],
195
+ { description: "Strategy: sma_crossover (buy when SMA20 > SMA50, sell on reverse), sma_50_200_crossover (buy when SMA50 > SMA200, sell on reverse), or rsi_mean_reversion (buy when RSI < 30, sell when RSI > 70)" },
192
196
  ),
193
197
  period: Type.Optional(
194
198
  Type.String({ description: "Historical period to backtest: 1y, 2y, 5y. Default: 2y" }),
@@ -199,9 +203,9 @@ export const backtestTool: AgentTool<typeof params> = {
199
203
  name: "backtest_strategy",
200
204
  label: "Backtest Strategy",
201
205
  description:
202
- "Backtest a simple trading strategy against historical data. Supported strategies: SMA crossover (SMA20/SMA50) and RSI mean-reversion (buy <30, sell >70). Returns total return, win rate, max drawdown, and comparison to buy-and-hold.",
206
+ "Backtest a simple trading strategy against historical data. Supported strategies: SMA crossover (SMA20/SMA50), standard long-term SMA crossover (SMA50/SMA200), and RSI mean-reversion (buy <30, sell >70). Returns total return, win rate, max drawdown, and comparison to buy-and-hold.",
203
207
  parameters: params,
204
- async execute(toolCallId, args) {
208
+ async execute(_toolCallId, args) {
205
209
  const symbol = args.symbol.toUpperCase();
206
210
  const period = args.period ?? "2y";
207
211
  const historyResult = await wrapProvider("yahoo", () => getHistory(symbol, period, "1d"));
@@ -213,9 +217,10 @@ export const backtestTool: AgentTool<typeof params> = {
213
217
  }
214
218
  const bars = historyResult.data;
215
219
 
216
- if (bars.length < 60) {
220
+ const minBars = requiredBarsForStrategy(args.strategy);
221
+ if (bars.length < minBars) {
217
222
  return {
218
- content: [{ type: "text", text: `Insufficient data for backtesting ${symbol} (need 60+ days, got ${bars.length})` }],
223
+ content: [{ type: "text", text: `Insufficient data for backtesting ${symbol} (need ${minBars}+ days, got ${bars.length})` }],
219
224
  details: null,
220
225
  };
221
226
  }
@@ -224,7 +229,7 @@ export const backtestTool: AgentTool<typeof params> = {
224
229
 
225
230
  const outperformance = result.totalReturn - result.buyAndHoldReturn;
226
231
  const lines = [
227
- `**${symbol} Backtest: ${args.strategy}** (${bars[0].date} to ${bars[bars.length - 1].date}, ${bars.length} days)`,
232
+ `**${symbol} Backtest: ${strategyLabel(args.strategy)}** (${bars[0].date} to ${bars[bars.length - 1].date}, ${bars.length} days)`,
228
233
  ``,
229
234
  `Strategy Return: ${(result.totalReturn * 100).toFixed(2)}%`,
230
235
  `Buy & Hold Return: ${(result.buyAndHoldReturn * 100).toFixed(2)}%`,
@@ -244,3 +249,19 @@ export const backtestTool: AgentTool<typeof params> = {
244
249
  };
245
250
  },
246
251
  };
252
+
253
+ function requiredBarsForStrategy(strategy: Strategy): number {
254
+ if (strategy === "sma_50_200_crossover") return 200;
255
+ return 60;
256
+ }
257
+
258
+ function strategyLabel(strategy: Strategy): string {
259
+ switch (strategy) {
260
+ case "sma_crossover":
261
+ return "SMA 20/50 Crossover";
262
+ case "sma_50_200_crossover":
263
+ return "SMA 50/200 Crossover";
264
+ case "rsi_mean_reversion":
265
+ return "RSI Mean Reversion";
266
+ }
267
+ }
@@ -46,7 +46,7 @@ export const technicalIndicatorsTool: AgentTool<typeof params> = {
46
46
  description:
47
47
  "Compute technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands) from historical price data. All computed locally — no API dependency.",
48
48
  parameters: params,
49
- async execute(toolCallId, args) {
49
+ async execute(_toolCallId, args) {
50
50
  const symbol = args.symbol.toUpperCase();
51
51
  const range = args.range ?? "1y";
52
52
  const result = await wrapProvider("yahoo", () => getHistory(symbol, range, "1d"));
@@ -68,8 +68,6 @@ export const technicalIndicatorsTool: AgentTool<typeof params> = {
68
68
 
69
69
  const sma20 = computeSMA(closes, 20);
70
70
  const sma50 = computeSMA(closes, 50);
71
- const ema12 = computeEMA(closes, 12);
72
- const ema26 = computeEMA(closes, 26);
73
71
  const rsi = computeRSI(closes, 14);
74
72
  const macd = computeMACD(closes);
75
73
  const bb = computeBollingerBands(closes, 20, 2);
@@ -21,6 +21,22 @@ export interface OptionContract {
21
21
  greeks: Greeks;
22
22
  }
23
23
 
24
+ export type OptionsMarketSession = "pre_market" | "regular" | "after_hours" | "closed";
25
+
26
+ export type OptionsBidAskState =
27
+ | "live_quotes"
28
+ | "closed_market_or_stale_quotes"
29
+ | "live_zero_bid_ask"
30
+ | "mixed_or_unknown";
31
+
32
+ export interface OptionsQuoteStatus {
33
+ marketSession: OptionsMarketSession;
34
+ bidAskState: OptionsBidAskState;
35
+ zeroBidAskContracts: number;
36
+ totalContracts: number;
37
+ warning?: string;
38
+ }
39
+
24
40
  export interface OptionsChain {
25
41
  symbol: string;
26
42
  underlyingPrice: number;
@@ -31,5 +47,6 @@ export interface OptionsChain {
31
47
  totalCallVolume: number;
32
48
  totalPutVolume: number;
33
49
  putCallRatio: number;
50
+ quoteStatus: OptionsQuoteStatus;
34
51
  fetchedAt: string;
35
52
  }
@@ -30,6 +30,38 @@ export interface RiskMetrics {
30
30
  var95: number; // 95% Value at Risk (daily)
31
31
  }
32
32
 
33
+ export interface FundHolding {
34
+ symbol: string;
35
+ name: string;
36
+ weight: number;
37
+ }
38
+
39
+ export interface FundHoldings {
40
+ symbol: string;
41
+ name?: string;
42
+ provider: string;
43
+ holdings: FundHolding[];
44
+ sectorWeights?: Record<string, number>;
45
+ }
46
+
47
+ export interface SharedFundHolding {
48
+ symbol: string;
49
+ name: string;
50
+ weights: Record<string, number>;
51
+ overlapWeight: number;
52
+ }
53
+
54
+ export interface FundOverlapPair {
55
+ symbols: [string, string];
56
+ overlapWeight: number;
57
+ sharedHoldings: SharedFundHolding[];
58
+ }
59
+
60
+ export interface FundHoldingsOverlap {
61
+ funds: FundHoldings[];
62
+ pairs: FundOverlapPair[];
63
+ }
64
+
33
65
  export interface TechnicalIndicators {
34
66
  symbol: string;
35
67
  period: string;