opencandle 0.10.0 → 0.11.1

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 (223) hide show
  1. package/README.md +12 -9
  2. package/dist/analysts/orchestrator.d.ts +3 -4
  3. package/dist/analysts/orchestrator.js +34 -44
  4. package/dist/analysts/orchestrator.js.map +1 -1
  5. package/dist/cli-main.js +242 -9
  6. package/dist/cli-main.js.map +1 -1
  7. package/dist/config.d.ts +4 -9
  8. package/dist/config.js +7 -10
  9. package/dist/config.js.map +1 -1
  10. package/dist/doctor/report.js +1 -1
  11. package/dist/doctor/report.js.map +1 -1
  12. package/dist/infra/cache.d.ts +6 -0
  13. package/dist/infra/cache.js +16 -6
  14. package/dist/infra/cache.js.map +1 -1
  15. package/dist/infra/freshness.d.ts +21 -0
  16. package/dist/infra/freshness.js +119 -0
  17. package/dist/infra/freshness.js.map +1 -0
  18. package/dist/infra/market-calendar.d.ts +14 -0
  19. package/dist/infra/market-calendar.js +137 -0
  20. package/dist/infra/market-calendar.js.map +1 -0
  21. package/dist/infra/rate-limiter.js +1 -0
  22. package/dist/infra/rate-limiter.js.map +1 -1
  23. package/dist/market-state/daily-report.d.ts +1 -1
  24. package/dist/market-state/daily-report.js +5 -16
  25. package/dist/market-state/daily-report.js.map +1 -1
  26. package/dist/market-state/service.d.ts +0 -35
  27. package/dist/market-state/service.js +0 -63
  28. package/dist/market-state/service.js.map +1 -1
  29. package/dist/market-state/summaries.d.ts +8 -0
  30. package/dist/market-state/summaries.js +70 -0
  31. package/dist/market-state/summaries.js.map +1 -0
  32. package/dist/memory/sqlite.js +20 -19
  33. package/dist/memory/sqlite.js.map +1 -1
  34. package/dist/onboarding/providers.d.ts +25 -1
  35. package/dist/onboarding/providers.js +26 -0
  36. package/dist/onboarding/providers.js.map +1 -1
  37. package/dist/pi/opencandle-extension.js +57 -226
  38. package/dist/pi/opencandle-extension.js.map +1 -1
  39. package/dist/pi/session-action-dedupe.d.ts +6 -0
  40. package/dist/pi/session-action-dedupe.js +126 -0
  41. package/dist/pi/session-action-dedupe.js.map +1 -0
  42. package/dist/pi/session-writer-lock.d.ts +26 -2
  43. package/dist/pi/session-writer-lock.js +230 -18
  44. package/dist/pi/session-writer-lock.js.map +1 -1
  45. package/dist/pi/setup.js +5 -5
  46. package/dist/pi/setup.js.map +1 -1
  47. package/dist/pi/tui-session-coordinator.d.ts +15 -0
  48. package/dist/pi/tui-session-coordinator.js +283 -0
  49. package/dist/pi/tui-session-coordinator.js.map +1 -0
  50. package/dist/prompts/context-builder.js +1 -1
  51. package/dist/prompts/policy-cards.js +1 -1
  52. package/dist/prompts/policy-cards.js.map +1 -1
  53. package/dist/providers/alpha-vantage.js +6 -0
  54. package/dist/providers/alpha-vantage.js.map +1 -1
  55. package/dist/providers/coingecko.js +1 -0
  56. package/dist/providers/coingecko.js.map +1 -1
  57. package/dist/providers/polymarket.d.ts +2 -0
  58. package/dist/providers/polymarket.js +163 -0
  59. package/dist/providers/polymarket.js.map +1 -0
  60. package/dist/providers/wrap-provider.js +5 -3
  61. package/dist/providers/wrap-provider.js.map +1 -1
  62. package/dist/providers/yahoo-finance.d.ts +3 -0
  63. package/dist/providers/yahoo-finance.js +206 -10
  64. package/dist/providers/yahoo-finance.js.map +1 -1
  65. package/dist/routing/classify-intent.js +4 -5
  66. package/dist/routing/classify-intent.js.map +1 -1
  67. package/dist/routing/entity-extractor.js +16 -0
  68. package/dist/routing/entity-extractor.js.map +1 -1
  69. package/dist/routing/route-manifest.js +1 -2
  70. package/dist/routing/route-manifest.js.map +1 -1
  71. package/dist/routing/router-llm-client.js +18 -3
  72. package/dist/routing/router-llm-client.js.map +1 -1
  73. package/dist/routing/router-prompt.js +1 -1
  74. package/dist/routing/router.d.ts +1 -1
  75. package/dist/routing/router.js +309 -9
  76. package/dist/routing/router.js.map +1 -1
  77. package/dist/routing/slot-resolver.d.ts +1 -0
  78. package/dist/routing/slot-resolver.js +1 -1
  79. package/dist/routing/slot-resolver.js.map +1 -1
  80. package/dist/runtime/evidence.d.ts +1 -0
  81. package/dist/runtime/evidence.js.map +1 -1
  82. package/dist/runtime/numeric-claims.d.ts +23 -0
  83. package/dist/runtime/numeric-claims.js +100 -0
  84. package/dist/runtime/numeric-claims.js.map +1 -0
  85. package/dist/runtime/planning-evidence.js +1 -76
  86. package/dist/runtime/planning-evidence.js.map +1 -1
  87. package/dist/runtime/prompt-step.d.ts +10 -1
  88. package/dist/runtime/prompt-step.js +116 -2
  89. package/dist/runtime/prompt-step.js.map +1 -1
  90. package/dist/runtime/session-coordinator.d.ts +11 -1
  91. package/dist/runtime/session-coordinator.js +354 -62
  92. package/dist/runtime/session-coordinator.js.map +1 -1
  93. package/dist/runtime/validation.d.ts +3 -1
  94. package/dist/runtime/workflow-events.d.ts +1 -1
  95. package/dist/runtime/workflow-events.js.map +1 -1
  96. package/dist/runtime/workflow-types.d.ts +3 -0
  97. package/dist/runtime/workflow-types.js.map +1 -1
  98. package/dist/system-prompt.js +1 -1
  99. package/dist/tools/fundamentals/comps.d.ts +1 -0
  100. package/dist/tools/fundamentals/comps.js +20 -9
  101. package/dist/tools/fundamentals/comps.js.map +1 -1
  102. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  103. package/dist/tools/fundamentals/dcf.js +190 -76
  104. package/dist/tools/fundamentals/dcf.js.map +1 -1
  105. package/dist/tools/index.d.ts +1 -1
  106. package/dist/tools/index.js +3 -3
  107. package/dist/tools/index.js.map +1 -1
  108. package/dist/tools/macro/event-probabilities.d.ts +8 -0
  109. package/dist/tools/macro/event-probabilities.js +121 -0
  110. package/dist/tools/macro/event-probabilities.js.map +1 -0
  111. package/dist/tools/market/crypto-price.d.ts +4 -1
  112. package/dist/tools/market/crypto-price.js +10 -1
  113. package/dist/tools/market/crypto-price.js.map +1 -1
  114. package/dist/tools/market/screen-stocks.js +14 -3
  115. package/dist/tools/market/screen-stocks.js.map +1 -1
  116. package/dist/tools/market/stock-quote.d.ts +4 -1
  117. package/dist/tools/market/stock-quote.js +9 -4
  118. package/dist/tools/market/stock-quote.js.map +1 -1
  119. package/dist/tools/options/option-chain.d.ts +4 -1
  120. package/dist/tools/options/option-chain.js +12 -1
  121. package/dist/tools/options/option-chain.js.map +1 -1
  122. package/dist/tools/portfolio/daily-report.js +10 -4
  123. package/dist/tools/portfolio/daily-report.js.map +1 -1
  124. package/dist/tools/portfolio/risk-analysis.js +1 -1
  125. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  126. package/dist/tools/technical/backtest.d.ts +23 -5
  127. package/dist/tools/technical/backtest.js +131 -94
  128. package/dist/tools/technical/backtest.js.map +1 -1
  129. package/dist/tools/technical/indicators.js +1 -1
  130. package/dist/tools/technical/indicators.js.map +1 -1
  131. package/dist/types/fundamentals.d.ts +1 -0
  132. package/dist/types/index.d.ts +1 -0
  133. package/dist/types/market.d.ts +2 -0
  134. package/dist/types/options.d.ts +1 -0
  135. package/dist/types/prediction-markets.d.ts +13 -0
  136. package/dist/types/prediction-markets.js +2 -0
  137. package/dist/types/prediction-markets.js.map +1 -0
  138. package/gui/server/chat-event-adapter.ts +89 -5
  139. package/gui/server/http-routes.ts +661 -37
  140. package/gui/server/invoke-tool.ts +165 -16
  141. package/gui/server/live-chat-event-adapter.ts +37 -2
  142. package/gui/server/local-session-coordinator.ts +97 -0
  143. package/gui/server/market-state-api.ts +64 -48
  144. package/gui/server/projector.ts +132 -3
  145. package/gui/server/server.ts +51 -6
  146. package/gui/server/session-actions.ts +146 -45
  147. package/gui/server/session-entry-wait.ts +22 -3
  148. package/gui/server/tool-metadata.ts +3 -1
  149. package/gui/server/ws-hub.ts +77 -10
  150. package/gui/shared/chat-events.ts +10 -1
  151. package/gui/shared/event-reducer.ts +1 -0
  152. package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
  153. package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
  154. package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
  155. package/gui/web/dist/index.html +2 -2
  156. package/package.json +13 -15
  157. package/src/analysts/orchestrator.ts +35 -56
  158. package/src/cli-main.ts +253 -13
  159. package/src/config.ts +12 -20
  160. package/src/doctor/report.ts +1 -1
  161. package/src/infra/cache.ts +24 -11
  162. package/src/infra/freshness.ts +165 -0
  163. package/src/infra/market-calendar.ts +193 -0
  164. package/src/infra/rate-limiter.ts +1 -0
  165. package/src/market-state/daily-report.ts +6 -16
  166. package/src/market-state/service.ts +0 -136
  167. package/src/market-state/summaries.ts +75 -0
  168. package/src/memory/sqlite.ts +23 -19
  169. package/src/onboarding/providers.ts +29 -1
  170. package/src/pi/opencandle-extension.ts +67 -270
  171. package/src/pi/session-action-dedupe.ts +155 -0
  172. package/src/pi/session-writer-lock.ts +290 -20
  173. package/src/pi/setup.ts +6 -6
  174. package/src/pi/tui-session-coordinator.ts +351 -0
  175. package/src/prompts/context-builder.ts +1 -1
  176. package/src/prompts/policy-cards.ts +1 -1
  177. package/src/providers/alpha-vantage.ts +6 -0
  178. package/src/providers/coingecko.ts +2 -0
  179. package/src/providers/polymarket.ts +214 -0
  180. package/src/providers/wrap-provider.ts +5 -3
  181. package/src/providers/yahoo-finance.ts +339 -27
  182. package/src/routing/classify-intent.ts +4 -5
  183. package/src/routing/entity-extractor.ts +17 -0
  184. package/src/routing/route-manifest.ts +1 -2
  185. package/src/routing/router-llm-client.ts +31 -19
  186. package/src/routing/router-prompt.ts +1 -1
  187. package/src/routing/router.ts +379 -8
  188. package/src/routing/slot-resolver.ts +1 -1
  189. package/src/runtime/evidence.ts +1 -0
  190. package/src/runtime/numeric-claims.ts +108 -0
  191. package/src/runtime/planning-evidence.ts +6 -91
  192. package/src/runtime/prompt-step.ts +127 -2
  193. package/src/runtime/session-coordinator.ts +464 -74
  194. package/src/runtime/validation.ts +1 -1
  195. package/src/runtime/workflow-events.ts +2 -0
  196. package/src/runtime/workflow-types.ts +3 -0
  197. package/src/system-prompt.ts +1 -1
  198. package/src/tools/AGENTS.md +1 -1
  199. package/src/tools/fundamentals/comps.ts +25 -14
  200. package/src/tools/fundamentals/dcf.ts +217 -87
  201. package/src/tools/index.ts +3 -3
  202. package/src/tools/macro/event-probabilities.ts +141 -0
  203. package/src/tools/market/crypto-price.ts +14 -2
  204. package/src/tools/market/screen-stocks.ts +14 -3
  205. package/src/tools/market/stock-quote.ts +49 -43
  206. package/src/tools/options/option-chain.ts +16 -2
  207. package/src/tools/portfolio/daily-report.ts +10 -4
  208. package/src/tools/portfolio/risk-analysis.ts +1 -1
  209. package/src/tools/technical/backtest.ts +167 -108
  210. package/src/tools/technical/indicators.ts +1 -1
  211. package/src/types/fundamentals.ts +1 -0
  212. package/src/types/index.ts +1 -0
  213. package/src/types/market.ts +2 -0
  214. package/src/types/options.ts +1 -0
  215. package/src/types/prediction-markets.ts +13 -0
  216. package/dist/tools/portfolio/predictions.d.ts +0 -55
  217. package/dist/tools/portfolio/predictions.js +0 -422
  218. package/dist/tools/portfolio/predictions.js.map +0 -1
  219. package/gui/server/chat-run-session.ts +0 -16
  220. package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
  221. package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
  222. package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
  223. package/src/tools/portfolio/predictions.ts +0 -553
@@ -19,7 +19,7 @@ You are an analyst, not a fiduciary advisor. When asked for entry levels, price
19
19
  - **Macro**: get_economic_data, get_fear_greed — FRED economic indicators and market sentiment
20
20
  - **Sentiment**: get_reddit_sentiment, get_twitter_sentiment, get_web_sentiment, get_sentiment_trend, get_sentiment_summary — retail and news sentiment from Reddit, Twitter/X, and web sources with historical trends
21
21
  - **Options**: get_option_chain — full options chain with strikes, bids/asks, volume, OI, IV, and computed Greeks (delta, gamma, theta, vega, rho)
22
- - **Portfolio**: track_portfolio, analyze_risk, manage_watchlist, analyze_correlation, track_prediction, manage_alerts, daily_watchlist_report, manage_notifications — position tracking, P&L, Sharpe ratio, VaR, watchlist tracking, durable local alerts, daily watchlist reports, notification history, correlation matrix, and prediction tracking with accuracy scoring
22
+ - **Portfolio**: track_portfolio, analyze_risk, manage_watchlist, analyze_correlation, manage_alerts, daily_watchlist_report, manage_notifications — position tracking, P&L, Sharpe ratio, VaR, watchlist tracking, durable local alerts, daily watchlist reports, notification history, and correlation matrix
23
23
  - **User Interaction**: ask_user — ask clarification questions and setup confirmations
24
24
 
25
25
  ## Analytical Framework
@@ -10,7 +10,7 @@ src/tools/
10
10
  ├── options/ # Options chains, Greeks computation
11
11
  ├── macro/ # FRED economic data, fear & greed index
12
12
  ├── sentiment/ # Reddit sentiment, news sentiment
13
- ├── portfolio/ # Tracker, risk analysis, watchlist, correlation, predictions
13
+ ├── portfolio/ # Tracker, risk analysis, watchlist, correlation
14
14
  ├── market/ # Stock quotes, history, crypto, ticker search
15
15
  └── index.ts # getAllTools() registry — add new tools here
16
16
  ```
@@ -4,6 +4,7 @@ import { getConfig } from "../../config.js";
4
4
  import { withCredentialCheck } from "../../onboarding/tool-helpers.js";
5
5
  import { getOverview } from "../../providers/alpha-vantage.js";
6
6
  import { wrapProvider } from "../../providers/wrap-provider.js";
7
+ import { getYahooCompanyOverview } from "../../providers/yahoo-finance.js";
7
8
  import type { CompanyOverview } from "../../types/fundamentals.js";
8
9
 
9
10
  export interface CompsMetric {
@@ -20,6 +21,7 @@ export interface CompsResult {
20
21
  companies: CompanyOverview[];
21
22
  metrics: CompsMetric[];
22
23
  unavailableSymbols: string[];
24
+ unavailableReasons: Record<string, string>;
23
25
  }
24
26
 
25
27
  type MetricDef = {
@@ -65,7 +67,7 @@ export function computeComps(companies: CompanyOverview[]): CompsResult {
65
67
  return { metric: def.name, values, median, p25, p75, best, worst };
66
68
  });
67
69
 
68
- return { companies, metrics, unavailableSymbols: [] };
70
+ return { companies, metrics, unavailableSymbols: [], unavailableReasons: {} };
69
71
  }
70
72
 
71
73
  function computeMedian(sorted: number[]): number | null {
@@ -103,23 +105,26 @@ export const compsTool: AgentTool<typeof params> = {
103
105
  const config = getConfig();
104
106
  const symbols = args.symbols.map((s) => s.toUpperCase());
105
107
 
106
- const results = await Promise.all(
107
- symbols.map(async (s) => ({
108
- symbol: s,
109
- result: await wrapProvider("alphavantage", () =>
110
- getOverview(s, config.alphaVantageApiKey!),
111
- ),
112
- })),
113
- );
114
-
115
108
  const companies: CompanyOverview[] = [];
116
109
  const unavailableSymbols: string[] = [];
110
+ const unavailableReasons: Record<string, string> = {};
111
+
112
+ for (const sym of symbols) {
113
+ const alphaResult = await wrapProvider("alphavantage", () =>
114
+ getOverview(sym, config.alphaVantageApiKey!),
115
+ );
116
+ if (alphaResult.status === "ok") {
117
+ companies.push(alphaResult.data);
118
+ continue;
119
+ }
117
120
 
118
- for (const { symbol: sym, result: r } of results) {
119
- if (r.status === "ok") {
120
- companies.push(r.data);
121
+ const yahooResult = await wrapProvider("yahoo", () => getYahooCompanyOverview(sym));
122
+ if (yahooResult.status === "ok") {
123
+ companies.push(yahooResult.data);
121
124
  } else {
122
125
  unavailableSymbols.push(sym);
126
+ unavailableReasons[sym] =
127
+ `Alpha Vantage: ${alphaResult.reason}; Yahoo Finance: ${yahooResult.reason}`;
123
128
  }
124
129
  }
125
130
 
@@ -137,6 +142,7 @@ export const compsTool: AgentTool<typeof params> = {
137
142
 
138
143
  const result = computeComps(companies);
139
144
  result.unavailableSymbols = unavailableSymbols;
145
+ result.unavailableReasons = unavailableReasons;
140
146
 
141
147
  const availableSymbols = companies.map((company) => company.symbol);
142
148
  const header = `**Comparable Company Analysis**: ${availableSymbols.join(" vs ")}`;
@@ -161,9 +167,14 @@ export const compsTool: AgentTool<typeof params> = {
161
167
  unavailableSymbols.length > 0
162
168
  ? ["", `Unavailable fundamentals: ${unavailableSymbols.join(", ")}`]
163
169
  : [];
170
+ const reasonLines =
171
+ unavailableSymbols.length > 0
172
+ ? unavailableSymbols.map((symbol) => ` - ${symbol} (${unavailableReasons[symbol]})`)
173
+ : [];
164
174
  const text = [header, "", symHeader, ...rows, ...noteLines].join("\n");
175
+ const textWithReasons = reasonLines.length > 0 ? `${text}\n${reasonLines.join("\n")}` : text;
165
176
 
166
- return { content: [{ type: "text", text }], details: result };
177
+ return { content: [{ type: "text", text: textWithReasons }], details: result };
167
178
  });
168
179
  },
169
180
  };
@@ -1,10 +1,11 @@
1
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
2
2
  import { Type } from "@sinclair/typebox";
3
3
  import { getConfig } from "../../config.js";
4
- import { withCredentialCheck } from "../../onboarding/tool-helpers.js";
5
4
  import { getFinancials, getOverview } from "../../providers/alpha-vantage.js";
5
+ import { ProviderCredentialError } from "../../providers/provider-credential-error.js";
6
6
  import { wrapProvider } from "../../providers/wrap-provider.js";
7
- import { getQuote } from "../../providers/yahoo-finance.js";
7
+ import { getQuote, getYahooFinancials } from "../../providers/yahoo-finance.js";
8
+ import type { ProviderResult } from "../../runtime/evidence.js";
8
9
  import type { FinancialStatement } from "../../types/fundamentals.js";
9
10
 
10
11
  export interface DCFResult {
@@ -45,6 +46,14 @@ export function computeDCF(params: DCFParams): DCFResult {
45
46
  sharesOutstanding,
46
47
  } = params;
47
48
 
49
+ // Gordon Growth requires discount > terminal growth; a non-positive spread
50
+ // divides by zero or flips the terminal value's sign.
51
+ if (discountRate <= terminalGrowth) {
52
+ throw new Error(
53
+ `Gordon Growth constraint violated: terminal growth (${(terminalGrowth * 100).toFixed(1)}%) must be below the discount rate (${(discountRate * 100).toFixed(1)}%).`,
54
+ );
55
+ }
56
+
48
57
  // Project future cash flows (mid-year convention: discount at year-0.5)
49
58
  const projectedCashFlows: Array<{ year: number; fcf: number; presentValue: number }> = [];
50
59
  for (let y = 1; y <= years; y++) {
@@ -152,12 +161,36 @@ function computeDCFSimple(
152
161
  return (sumPV + pvTV - debt) / shares;
153
162
  }
154
163
 
155
- export function computeNetDebt(f: FinancialStatement): number {
164
+ export function computeNetDebt(f: FinancialStatement): number | null {
156
165
  if (f.totalDebt != null && f.cashAndEquivalents != null) {
157
166
  return f.totalDebt - f.cashAndEquivalents;
158
167
  }
159
- // Fallback: totalLiabilities - totalAssets (negative means net cash position)
160
- return f.totalLiabilities - f.totalAssets;
168
+ return null;
169
+ }
170
+
171
+ async function optionalAlphaVantage<T>(
172
+ apiKey: string | undefined,
173
+ fn: (apiKey: string) => Promise<T>,
174
+ ): Promise<ProviderResult<T>> {
175
+ if (!apiKey) {
176
+ return {
177
+ status: "unavailable",
178
+ reason: "Alpha Vantage API key not configured",
179
+ provider: "alphavantage",
180
+ };
181
+ }
182
+ try {
183
+ return await wrapProvider("alphavantage", () => fn(apiKey));
184
+ } catch (error) {
185
+ if (error instanceof ProviderCredentialError) {
186
+ return {
187
+ status: "unavailable",
188
+ reason: `Alpha Vantage credential ${error.reason}`,
189
+ provider: "alphavantage",
190
+ };
191
+ }
192
+ throw error;
193
+ }
161
194
  }
162
195
 
163
196
  const params = Type.Object({
@@ -186,113 +219,210 @@ export const dcfTool: AgentTool<typeof params> = {
186
219
  "Compute a Discounted Cash Flow (DCF) intrinsic value estimate for a stock. Uses free cash flow, growth projections, and a discount rate to estimate what the stock is worth. Returns intrinsic value per share, margin of safety vs current price, and a sensitivity table.",
187
220
  parameters: params,
188
221
  async execute(_toolCallId, args) {
189
- return withCredentialCheck("alpha_vantage", async () => {
190
- const symbol = args.symbol.toUpperCase();
191
- const config = getConfig();
192
-
193
- const [overviewResult, financialsResult, quoteResult] = await Promise.all([
194
- wrapProvider("alphavantage", () => getOverview(symbol, config.alphaVantageApiKey!)),
195
- wrapProvider("alphavantage", () => getFinancials(symbol, config.alphaVantageApiKey!)),
196
- wrapProvider("yahoo", () => getQuote(symbol)),
197
- ]);
198
-
199
- const missing: string[] = [];
200
- if (overviewResult.status === "unavailable")
201
- missing.push(`company overview (${overviewResult.reason})`);
202
- if (financialsResult.status === "unavailable")
203
- missing.push(`financial statements (${financialsResult.reason})`);
204
- if (quoteResult.status === "unavailable") missing.push(`stock quote (${quoteResult.reason})`);
205
-
206
- if (financialsResult.status === "unavailable" || quoteResult.status === "unavailable") {
222
+ const symbol = args.symbol.toUpperCase();
223
+ const config = getConfig();
224
+ const alphaVantageApiKey = config.alphaVantageApiKey;
225
+
226
+ const [alphaVantageFinancialsResult, quoteResult] = await Promise.all([
227
+ optionalAlphaVantage(alphaVantageApiKey, (apiKey) => getFinancials(symbol, apiKey)),
228
+ wrapProvider("yahoo", () => getQuote(symbol)),
229
+ ]);
230
+
231
+ const missing: string[] = [];
232
+ if (quoteResult.status === "unavailable") missing.push(`stock quote (${quoteResult.reason})`);
233
+
234
+ if (quoteResult.status === "unavailable") {
235
+ return {
236
+ content: [
237
+ {
238
+ type: "text",
239
+ text: `⚠ DCF valuation unavailable for ${symbol}. Missing: ${missing.join(", ")}. Both financials and current price are required.`,
240
+ },
241
+ ],
242
+ details: null,
243
+ };
244
+ }
245
+
246
+ const quote = quoteResult.data;
247
+ if (quote.price <= 0) {
248
+ return {
249
+ content: [
250
+ {
251
+ type: "text",
252
+ text: `⚠ DCF valuation unavailable for ${symbol}. Missing: current stock price. DCF output requires a real current quote price.`,
253
+ },
254
+ ],
255
+ details: null,
256
+ };
257
+ }
258
+
259
+ let financialsSource = "Alpha Vantage";
260
+ let financials: FinancialStatement[];
261
+ if (alphaVantageFinancialsResult.status === "ok" && !alphaVantageFinancialsResult.stale) {
262
+ financials = alphaVantageFinancialsResult.data;
263
+ } else {
264
+ const alphaVantageReason =
265
+ alphaVantageFinancialsResult.status === "ok"
266
+ ? `stale cached financial statements${
267
+ alphaVantageFinancialsResult.timestamp
268
+ ? ` from ${alphaVantageFinancialsResult.timestamp}`
269
+ : ""
270
+ }`
271
+ : alphaVantageFinancialsResult.reason;
272
+ const yahooFinancialsResult = await wrapProvider("yahoo", () => getYahooFinancials(symbol));
273
+ if (yahooFinancialsResult.status === "unavailable") {
207
274
  return {
208
275
  content: [
209
276
  {
210
277
  type: "text",
211
- text: `⚠ DCF valuation unavailable for ${symbol}. Missing: ${missing.join(", ")}. Both financials and current price are required.`,
278
+ text: `⚠ DCF valuation unavailable for ${symbol}. Missing: financial statements (Alpha Vantage: ${alphaVantageReason}; Yahoo Finance: ${yahooFinancialsResult.reason}). Both financials and current price are required.`,
212
279
  },
213
280
  ],
214
281
  details: null,
215
282
  };
216
283
  }
217
-
218
- const overview = overviewResult.status === "ok" ? overviewResult.data : null;
219
- const financials = financialsResult.data;
220
- const quote = quoteResult.data;
221
-
222
- const latestFCF = financials[0]?.freeCashFlow ?? 0;
223
- if (latestFCF <= 0) {
284
+ if (yahooFinancialsResult.stale) {
285
+ const cachedAt = yahooFinancialsResult.timestamp
286
+ ? ` from ${yahooFinancialsResult.timestamp}`
287
+ : "";
224
288
  return {
225
289
  content: [
226
290
  {
227
291
  type: "text",
228
- text: `${symbol} has negative or zero free cash flow ($${latestFCF.toLocaleString()}). DCF is not meaningful for companies without positive FCF.`,
292
+ text: `⚠ DCF valuation unavailable for ${symbol}. Yahoo Finance returned stale cached financial statements${cachedAt} after Alpha Vantage could not provide fresh financial statements (${alphaVantageReason}). DCF output requires fresh financial statements and a current price.`,
229
293
  },
230
294
  ],
231
295
  details: null,
232
296
  };
233
297
  }
298
+ financials = yahooFinancialsResult.data;
299
+ financialsSource = "Yahoo Finance";
300
+ }
301
+
302
+ const latestFCF = financials[0]?.freeCashFlow ?? 0;
303
+ if (latestFCF <= 0) {
304
+ return {
305
+ content: [
306
+ {
307
+ type: "text",
308
+ text: `${symbol} has negative or zero free cash flow ($${latestFCF.toLocaleString()}). DCF is not meaningful for companies without positive FCF.`,
309
+ },
310
+ ],
311
+ details: null,
312
+ };
313
+ }
234
314
 
235
- // Estimate growth from historical FCF if not provided
236
- let growthRate = args.growth_rate ?? 0.1;
237
- if (!args.growth_rate && financials.length >= 2) {
238
- const olderFCF = financials[1]?.freeCashFlow;
239
- if (olderFCF && olderFCF > 0) {
240
- growthRate = Math.max(0.02, Math.min(0.25, (latestFCF - olderFCF) / olderFCF));
241
- }
315
+ // Estimate growth from historical FCF if not provided
316
+ let growthRate = args.growth_rate ?? 0.1;
317
+ if (!args.growth_rate && financials.length >= 2) {
318
+ const olderFCF = financials[1]?.freeCashFlow;
319
+ if (olderFCF && olderFCF > 0) {
320
+ growthRate = Math.max(0.02, Math.min(0.25, (latestFCF - olderFCF) / olderFCF));
242
321
  }
322
+ }
243
323
 
244
- const discountRate = args.discount_rate ?? 0.1;
245
- const terminalGrowth = args.terminal_growth ?? 0.03;
246
- const years = args.projection_years ?? 5;
247
- const marketCap = overview?.marketCap ?? 0;
248
- const sharesOutstanding = quote.price > 0 && marketCap > 0 ? marketCap / quote.price : 1;
249
- const netDebt = financials[0] ? computeNetDebt(financials[0]) : 0;
250
-
251
- const result = computeDCF({
252
- freeCashFlow: latestFCF,
253
- growthRate,
254
- discountRate,
255
- terminalGrowth,
256
- years,
257
- netDebt: Math.max(0, netDebt),
258
- sharesOutstanding,
259
- });
260
-
261
- const marginOfSafety = (result.intrinsicValue - quote.price) / result.intrinsicValue;
262
- const upside = (result.intrinsicValue - quote.price) / quote.price;
263
-
264
- const lines = [
265
- `**${symbol} DCF Valuation**`,
266
- ``,
267
- `Current Price: $${quote.price.toFixed(2)}`,
268
- `Intrinsic Value: $${result.intrinsicValue.toFixed(2)}`,
269
- `Margin of Safety: ${(marginOfSafety * 100).toFixed(1)}%`,
270
- `Upside/Downside: ${upside >= 0 ? "+" : ""}${(upside * 100).toFixed(1)}%`,
271
- ``,
272
- `**Assumptions**`,
273
- `Free Cash Flow: $${(latestFCF / 1e9).toFixed(2)}B`,
274
- `Growth Rate: ${(growthRate * 100).toFixed(1)}%`,
275
- `Discount Rate (WACC): ${(discountRate * 100).toFixed(1)}%`,
276
- `Terminal Growth: ${(terminalGrowth * 100).toFixed(1)}%`,
277
- `Projection: ${years} years`,
278
- ``,
279
- `**Projected Cash Flows**`,
280
- ...result.projectedCashFlows.map(
281
- (cf) =>
282
- ` Year ${cf.year}: FCF $${(cf.fcf / 1e9).toFixed(2)}B → PV $${(cf.presentValue / 1e9).toFixed(2)}B`,
283
- ),
284
- ` Terminal Value: $${(result.terminalValue / 1e9).toFixed(2)}B`,
285
- ` Enterprise Value: $${(result.enterpriseValue / 1e9).toFixed(2)}B`,
286
- ``,
287
- `**Sensitivity Table** (Intrinsic Value at different Growth/Discount rates)`,
288
- ...formatSensitivityTable(result.sensitivityTable),
289
- ];
324
+ const discountRate = args.discount_rate ?? 0.1;
325
+ const terminalGrowth = args.terminal_growth ?? 0.03;
326
+ if (discountRate <= terminalGrowth) {
327
+ return {
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: `⚠ Invalid DCF assumptions for ${symbol}: terminal growth (${(terminalGrowth * 100).toFixed(1)}%) must be below the discount rate (${(discountRate * 100).toFixed(1)}%) for the Gordon Growth terminal value to be meaningful. Lower terminal_growth or raise discount_rate.`,
332
+ },
333
+ ],
334
+ details: null,
335
+ };
336
+ }
290
337
 
338
+ const years = args.projection_years ?? 5;
339
+ // Prefer an actual reported share count, then derive from market cap
340
+ // only when needed. Never substitute a placeholder share count.
341
+ const statementSharesOutstanding = financials[0]?.sharesOutstanding;
342
+ let marketCap = quote.marketCap > 0 ? quote.marketCap : 0;
343
+ let sharesOutstanding =
344
+ statementSharesOutstanding && statementSharesOutstanding > 0 ? statementSharesOutstanding : 0;
345
+ if (sharesOutstanding <= 0 && marketCap <= 0) {
346
+ const overviewResult = await optionalAlphaVantage(alphaVantageApiKey, (apiKey) =>
347
+ getOverview(symbol, apiKey),
348
+ );
349
+ if (overviewResult.status === "ok" && overviewResult.data.marketCap > 0) {
350
+ marketCap = overviewResult.data.marketCap;
351
+ }
352
+ }
353
+ if (sharesOutstanding <= 0 && quote.price > 0 && marketCap > 0) {
354
+ sharesOutstanding = marketCap / quote.price;
355
+ }
356
+ if (sharesOutstanding <= 0) {
291
357
  return {
292
- content: [{ type: "text", text: lines.join("\n") }],
293
- details: { ...result, currentPrice: quote.price, marginOfSafety, upside },
358
+ content: [
359
+ {
360
+ type: "text",
361
+ text: `⚠ Cannot compute a per-share DCF for ${symbol}: shares outstanding are unavailable from financial statements and cannot be derived because ${quote.price <= 0 ? "the current quote price is unavailable" : "market capitalization is unavailable from the overview and quote providers"}. Per-share intrinsic value requires a real share count.`,
362
+ },
363
+ ],
364
+ details: null,
294
365
  };
366
+ }
367
+ // Signed on purpose: net cash (negative net debt) adds to equity value.
368
+ // Omit the adjustment if debt/cash fields are missing; assets minus
369
+ // liabilities is book equity, not a net-cash proxy.
370
+ const computedNetDebt = financials[0] ? computeNetDebt(financials[0]) : null;
371
+ const netDebt = computedNetDebt ?? 0;
372
+
373
+ const result = computeDCF({
374
+ freeCashFlow: latestFCF,
375
+ growthRate,
376
+ discountRate,
377
+ terminalGrowth,
378
+ years,
379
+ netDebt,
380
+ sharesOutstanding,
295
381
  });
382
+ if (computedNetDebt == null) {
383
+ result.warnings.push(
384
+ "Net debt adjustment omitted because total debt and cash equivalents were unavailable.",
385
+ );
386
+ }
387
+
388
+ const marginOfSafety = (result.intrinsicValue - quote.price) / result.intrinsicValue;
389
+ const upside = (result.intrinsicValue - quote.price) / quote.price;
390
+
391
+ const lines = [
392
+ `**${symbol} DCF Valuation**`,
393
+ ``,
394
+ `Current Price: $${quote.price.toFixed(2)}`,
395
+ `Intrinsic Value: $${result.intrinsicValue.toFixed(2)}`,
396
+ `Margin of Safety: ${(marginOfSafety * 100).toFixed(1)}%`,
397
+ `Upside/Downside: ${upside >= 0 ? "+" : ""}${(upside * 100).toFixed(1)}%`,
398
+ ``,
399
+ `**Assumptions**`,
400
+ `Financial statements source: ${financialsSource}`,
401
+ `Free Cash Flow: $${(latestFCF / 1e9).toFixed(2)}B`,
402
+ `Growth Rate: ${(growthRate * 100).toFixed(1)}%`,
403
+ `Discount Rate (WACC): ${(discountRate * 100).toFixed(1)}%`,
404
+ `Terminal Growth: ${(terminalGrowth * 100).toFixed(1)}%`,
405
+ `Projection: ${years} years`,
406
+ ``,
407
+ `**Projected Cash Flows**`,
408
+ ...result.projectedCashFlows.map(
409
+ (cf) =>
410
+ ` Year ${cf.year}: FCF $${(cf.fcf / 1e9).toFixed(2)}B → PV $${(cf.presentValue / 1e9).toFixed(2)}B`,
411
+ ),
412
+ ` Terminal Value: $${(result.terminalValue / 1e9).toFixed(2)}B`,
413
+ ` Enterprise Value: $${(result.enterpriseValue / 1e9).toFixed(2)}B`,
414
+ ``,
415
+ `**Sensitivity Table** (Intrinsic Value at different Growth/Discount rates)`,
416
+ ...formatSensitivityTable(result.sensitivityTable),
417
+ ...(result.warnings.length > 0
418
+ ? ["", "**Warnings**", ...result.warnings.map((warning) => `- ${warning}`)]
419
+ : []),
420
+ ];
421
+
422
+ return {
423
+ content: [{ type: "text", text: lines.join("\n") }],
424
+ details: { ...result, currentPrice: quote.price, marginOfSafety, upside },
425
+ };
296
426
  },
297
427
  };
298
428
 
@@ -6,6 +6,7 @@ import { dcfTool } from "./fundamentals/dcf.js";
6
6
  import { earningsTool } from "./fundamentals/earnings.js";
7
7
  import { financialsTool } from "./fundamentals/financials.js";
8
8
  import { secFilingsTool } from "./fundamentals/sec-filings.js";
9
+ import { eventProbabilitiesTool } from "./macro/event-probabilities.js";
9
10
  import { fearGreedTool } from "./macro/fear-greed.js";
10
11
  import { fredDataTool } from "./macro/fred-data.js";
11
12
  import { cryptoHistoryTool } from "./market/crypto-history.js";
@@ -20,7 +21,6 @@ import { correlationTool } from "./portfolio/correlation.js";
20
21
  import { dailyReportTool } from "./portfolio/daily-report.js";
21
22
  import { holdingsOverlapTool } from "./portfolio/holdings-overlap.js";
22
23
  import { notificationsTool } from "./portfolio/notifications.js";
23
- import { predictionsTool } from "./portfolio/predictions.js";
24
24
  import { riskAnalysisTool } from "./portfolio/risk-analysis.js";
25
25
  import { portfolioTrackerTool } from "./portfolio/tracker.js";
26
26
  import { watchlistTool } from "./portfolio/watchlist.js";
@@ -39,6 +39,7 @@ export { dcfTool } from "./fundamentals/dcf.js";
39
39
  export { earningsTool } from "./fundamentals/earnings.js";
40
40
  export { financialsTool } from "./fundamentals/financials.js";
41
41
  export { secFilingsTool } from "./fundamentals/sec-filings.js";
42
+ export { eventProbabilitiesTool } from "./macro/event-probabilities.js";
42
43
  export { fearGreedTool } from "./macro/fear-greed.js";
43
44
  export { fredDataTool } from "./macro/fred-data.js";
44
45
  export { cryptoHistoryTool } from "./market/crypto-history.js";
@@ -53,7 +54,6 @@ export { correlationTool } from "./portfolio/correlation.js";
53
54
  export { dailyReportTool } from "./portfolio/daily-report.js";
54
55
  export { holdingsOverlapTool } from "./portfolio/holdings-overlap.js";
55
56
  export { notificationsTool } from "./portfolio/notifications.js";
56
- export { predictionsTool } from "./portfolio/predictions.js";
57
57
  export { riskAnalysisTool } from "./portfolio/risk-analysis.js";
58
58
  export { portfolioTrackerTool } from "./portfolio/tracker.js";
59
59
  export { watchlistTool } from "./portfolio/watchlist.js";
@@ -80,6 +80,7 @@ export function getAllTools(options: { askUserHandler?: AskUserHandler } = {}):
80
80
  dcfTool,
81
81
  compsTool,
82
82
  secFilingsTool,
83
+ eventProbabilitiesTool,
83
84
  fredDataTool,
84
85
  fearGreedTool,
85
86
  createRedditSentimentTool({ askUserHandler: options.askUserHandler }),
@@ -91,7 +92,6 @@ export function getAllTools(options: { askUserHandler?: AskUserHandler } = {}):
91
92
  watchlistTool,
92
93
  correlationTool,
93
94
  holdingsOverlapTool,
94
- predictionsTool,
95
95
  alertsTool,
96
96
  dailyReportTool,
97
97
  notificationsTool,
@@ -0,0 +1,141 @@
1
+ import type { AgentTool } from "@earendil-works/pi-agent-core";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { searchPredictionMarkets } from "../../providers/polymarket.js";
4
+ import { wrapProvider } from "../../providers/wrap-provider.js";
5
+ import type { PredictionMarketQuote } from "../../types/prediction-markets.js";
6
+ import { renderUntrustedText, untrustedContentHeader } from "../sentiment/untrusted-text.js";
7
+
8
+ const LOW_LIQUIDITY_VOLUME_USD = 10_000;
9
+
10
+ const params = Type.Object({
11
+ query: Type.String({
12
+ minLength: 1,
13
+ description: "Free-text event question to search on Polymarket.",
14
+ }),
15
+ limit: Type.Optional(
16
+ Type.Integer({
17
+ minimum: 1,
18
+ maximum: 50,
19
+ description: "Maximum number of outcome quotes to return. Default: 8.",
20
+ }),
21
+ ),
22
+ });
23
+
24
+ export const eventProbabilitiesTool: AgentTool<typeof params, PredictionMarketQuote[]> = {
25
+ name: "get_event_probabilities",
26
+ label: "Event Probabilities",
27
+ description:
28
+ "Search Polymarket for market-implied event probabilities. Fetches and formats probabilities only; it does not analyze or forecast outcomes.",
29
+ parameters: params,
30
+ async execute(_toolCallId, args) {
31
+ const query = args.query.trim();
32
+ const limit = args.limit ?? 8;
33
+ const result = await wrapProvider("polymarket", () => searchPredictionMarkets(query, limit));
34
+
35
+ if (result.status === "unavailable") {
36
+ return {
37
+ content: [
38
+ {
39
+ type: "text",
40
+ text: [
41
+ `Polymarket prediction markets unavailable for "${query}" (${result.reason}).`,
42
+ "",
43
+ caveatText(),
44
+ ].join("\n"),
45
+ },
46
+ ],
47
+ details: [] as PredictionMarketQuote[],
48
+ };
49
+ }
50
+
51
+ const quotes = result.data;
52
+ const cacheDisclosure =
53
+ result.stale && result.timestamp
54
+ ? [
55
+ `Using cached Polymarket data from ${new Date(result.timestamp).toISOString()} because the provider fallback returned stale cache.`,
56
+ "",
57
+ ]
58
+ : [];
59
+ const text =
60
+ quotes.length === 0
61
+ ? [
62
+ ...cacheDisclosure,
63
+ `No Polymarket prediction markets found for "${query}".`,
64
+ "",
65
+ caveatText(),
66
+ ].join("\n")
67
+ : [
68
+ `**Polymarket event probabilities for "${query}"**`,
69
+ "",
70
+ ...cacheDisclosure,
71
+ untrustedContentHeader("Polymarket market text"),
72
+ "",
73
+ ...quotes.map(formatQuote),
74
+ "",
75
+ caveatText(),
76
+ ].join("\n");
77
+
78
+ return { content: [{ type: "text", text }], details: quotes.map(sanitizeQuoteDetails) };
79
+ },
80
+ };
81
+
82
+ function sanitizeQuoteDetails(quote: PredictionMarketQuote): PredictionMarketQuote {
83
+ return {
84
+ source: quote.source,
85
+ marketId: renderUntrustedText(quote.marketId, 120),
86
+ title: renderUntrustedText(quote.title, 220),
87
+ outcome: renderUntrustedText(quote.outcome, 80),
88
+ probability: quote.probability,
89
+ ...(quote.volumeUsd !== undefined && { volumeUsd: quote.volumeUsd }),
90
+ ...(quote.liquidityUsd !== undefined && { liquidityUsd: quote.liquidityUsd }),
91
+ url: renderUntrustedText(quote.url, 500),
92
+ ...(quote.asOf && { asOf: renderUntrustedText(quote.asOf, 80) }),
93
+ ...(quote.closeDate && { closeDate: renderUntrustedText(quote.closeDate, 80) }),
94
+ ...(quote.resolutionCriteria && {
95
+ resolutionCriteria: renderUntrustedText(quote.resolutionCriteria, 1000),
96
+ }),
97
+ };
98
+ }
99
+
100
+ function formatQuote(quote: PredictionMarketQuote): string {
101
+ return [
102
+ `- ${formatPercent(quote.probability)} - ${renderUntrustedText(quote.outcome, 80)}: ${renderUntrustedText(quote.title, 220)}`,
103
+ ` Volume: ${formatUsd(quote.volumeUsd)} | Liquidity: ${formatUsd(quote.liquidityUsd)} | Close: ${
104
+ quote.closeDate ? renderUntrustedText(quote.closeDate, 80) : "unknown"
105
+ }`,
106
+ ` Updated: ${quote.asOf ? renderUntrustedText(quote.asOf, 80) : "unavailable from Polymarket"}`,
107
+ quote.volumeUsd !== undefined && quote.volumeUsd < LOW_LIQUIDITY_VOLUME_USD
108
+ ? " LOW-LIQUIDITY: volume under $10,000"
109
+ : undefined,
110
+ ` Resolution criteria: ${
111
+ quote.resolutionCriteria
112
+ ? renderUntrustedText(quote.resolutionCriteria, 1000)
113
+ : "Resolution criteria unavailable from Polymarket."
114
+ }`,
115
+ ` URL: ${renderUntrustedText(quote.url, 500)}`,
116
+ ]
117
+ .filter(Boolean)
118
+ .join("\n");
119
+ }
120
+
121
+ function formatPercent(value: number): string {
122
+ return `${(value * 100).toFixed(1)}%`;
123
+ }
124
+
125
+ function formatUsd(value: number | undefined): string {
126
+ if (value === undefined) return "unavailable";
127
+ return new Intl.NumberFormat("en-US", {
128
+ style: "currency",
129
+ currency: "USD",
130
+ maximumFractionDigits: 2,
131
+ }).format(value);
132
+ }
133
+
134
+ function caveatText(): string {
135
+ return [
136
+ "Caveats:",
137
+ "- These are market-implied probabilities from trader positioning, not calibrated forecasts.",
138
+ "- Thin markets can show noisy or stale prices; results with volume under $10,000 are explicitly flagged low-liquidity.",
139
+ "- Polymarket is a crypto-settled venue with platform and settlement risks distinct from regulated exchanges.",
140
+ ].join("\n");
141
+ }