opencandle 0.11.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 (162) hide show
  1. package/README.md +5 -4
  2. package/dist/analysts/orchestrator.d.ts +2 -0
  3. package/dist/analysts/orchestrator.js +19 -0
  4. package/dist/analysts/orchestrator.js.map +1 -1
  5. package/dist/doctor/report.js +1 -1
  6. package/dist/doctor/report.js.map +1 -1
  7. package/dist/infra/cache.d.ts +6 -0
  8. package/dist/infra/cache.js +16 -6
  9. package/dist/infra/cache.js.map +1 -1
  10. package/dist/infra/freshness.d.ts +21 -0
  11. package/dist/infra/freshness.js +119 -0
  12. package/dist/infra/freshness.js.map +1 -0
  13. package/dist/infra/market-calendar.d.ts +14 -0
  14. package/dist/infra/market-calendar.js +137 -0
  15. package/dist/infra/market-calendar.js.map +1 -0
  16. package/dist/infra/rate-limiter.js +1 -0
  17. package/dist/infra/rate-limiter.js.map +1 -1
  18. package/dist/market-state/summaries.d.ts +8 -0
  19. package/dist/market-state/summaries.js +70 -0
  20. package/dist/market-state/summaries.js.map +1 -0
  21. package/dist/onboarding/providers.d.ts +25 -1
  22. package/dist/onboarding/providers.js +26 -0
  23. package/dist/onboarding/providers.js.map +1 -1
  24. package/dist/pi/opencandle-extension.js +44 -5
  25. package/dist/pi/opencandle-extension.js.map +1 -1
  26. package/dist/providers/alpha-vantage.js +6 -0
  27. package/dist/providers/alpha-vantage.js.map +1 -1
  28. package/dist/providers/coingecko.js +1 -0
  29. package/dist/providers/coingecko.js.map +1 -1
  30. package/dist/providers/polymarket.d.ts +2 -0
  31. package/dist/providers/polymarket.js +163 -0
  32. package/dist/providers/polymarket.js.map +1 -0
  33. package/dist/providers/wrap-provider.js +5 -3
  34. package/dist/providers/wrap-provider.js.map +1 -1
  35. package/dist/providers/yahoo-finance.d.ts +3 -0
  36. package/dist/providers/yahoo-finance.js +206 -10
  37. package/dist/providers/yahoo-finance.js.map +1 -1
  38. package/dist/routing/entity-extractor.js +16 -0
  39. package/dist/routing/entity-extractor.js.map +1 -1
  40. package/dist/routing/route-manifest.js +1 -1
  41. package/dist/routing/route-manifest.js.map +1 -1
  42. package/dist/routing/router-llm-client.js +18 -3
  43. package/dist/routing/router-llm-client.js.map +1 -1
  44. package/dist/routing/router.d.ts +1 -1
  45. package/dist/routing/router.js +274 -5
  46. package/dist/routing/router.js.map +1 -1
  47. package/dist/routing/slot-resolver.d.ts +1 -0
  48. package/dist/routing/slot-resolver.js +1 -1
  49. package/dist/routing/slot-resolver.js.map +1 -1
  50. package/dist/runtime/evidence.d.ts +1 -0
  51. package/dist/runtime/evidence.js.map +1 -1
  52. package/dist/runtime/numeric-claims.d.ts +23 -0
  53. package/dist/runtime/numeric-claims.js +100 -0
  54. package/dist/runtime/numeric-claims.js.map +1 -0
  55. package/dist/runtime/planning-evidence.js +1 -76
  56. package/dist/runtime/planning-evidence.js.map +1 -1
  57. package/dist/runtime/prompt-step.d.ts +10 -1
  58. package/dist/runtime/prompt-step.js +116 -2
  59. package/dist/runtime/prompt-step.js.map +1 -1
  60. package/dist/runtime/session-coordinator.d.ts +11 -1
  61. package/dist/runtime/session-coordinator.js +352 -49
  62. package/dist/runtime/session-coordinator.js.map +1 -1
  63. package/dist/runtime/validation.d.ts +3 -1
  64. package/dist/runtime/workflow-events.d.ts +1 -1
  65. package/dist/runtime/workflow-events.js.map +1 -1
  66. package/dist/runtime/workflow-types.d.ts +3 -0
  67. package/dist/runtime/workflow-types.js.map +1 -1
  68. package/dist/tools/fundamentals/comps.d.ts +1 -0
  69. package/dist/tools/fundamentals/comps.js +20 -9
  70. package/dist/tools/fundamentals/comps.js.map +1 -1
  71. package/dist/tools/fundamentals/dcf.js +183 -112
  72. package/dist/tools/fundamentals/dcf.js.map +1 -1
  73. package/dist/tools/index.d.ts +1 -0
  74. package/dist/tools/index.js +3 -0
  75. package/dist/tools/index.js.map +1 -1
  76. package/dist/tools/macro/event-probabilities.d.ts +8 -0
  77. package/dist/tools/macro/event-probabilities.js +121 -0
  78. package/dist/tools/macro/event-probabilities.js.map +1 -0
  79. package/dist/tools/market/crypto-price.d.ts +4 -1
  80. package/dist/tools/market/crypto-price.js +10 -1
  81. package/dist/tools/market/crypto-price.js.map +1 -1
  82. package/dist/tools/market/screen-stocks.js +14 -3
  83. package/dist/tools/market/screen-stocks.js.map +1 -1
  84. package/dist/tools/market/stock-quote.d.ts +4 -1
  85. package/dist/tools/market/stock-quote.js +9 -4
  86. package/dist/tools/market/stock-quote.js.map +1 -1
  87. package/dist/tools/options/option-chain.d.ts +4 -1
  88. package/dist/tools/options/option-chain.js +12 -1
  89. package/dist/tools/options/option-chain.js.map +1 -1
  90. package/dist/tools/portfolio/risk-analysis.js +1 -1
  91. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  92. package/dist/tools/technical/indicators.js +1 -1
  93. package/dist/tools/technical/indicators.js.map +1 -1
  94. package/dist/types/fundamentals.d.ts +1 -0
  95. package/dist/types/index.d.ts +1 -0
  96. package/dist/types/market.d.ts +2 -0
  97. package/dist/types/options.d.ts +1 -0
  98. package/dist/types/prediction-markets.d.ts +13 -0
  99. package/dist/types/prediction-markets.js +2 -0
  100. package/dist/types/prediction-markets.js.map +1 -0
  101. package/gui/server/chat-event-adapter.ts +89 -5
  102. package/gui/server/http-routes.ts +285 -37
  103. package/gui/server/invoke-tool.ts +14 -13
  104. package/gui/server/live-chat-event-adapter.ts +37 -2
  105. package/gui/server/market-state-api.ts +63 -5
  106. package/gui/server/projector.ts +132 -3
  107. package/gui/server/session-actions.ts +32 -40
  108. package/gui/server/session-entry-wait.ts +22 -3
  109. package/gui/server/ws-hub.ts +16 -4
  110. package/gui/shared/chat-events.ts +10 -1
  111. package/gui/shared/event-reducer.ts +1 -0
  112. package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
  113. package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
  114. package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
  115. package/gui/web/dist/index.html +2 -2
  116. package/package.json +8 -13
  117. package/src/analysts/orchestrator.ts +20 -0
  118. package/src/doctor/report.ts +1 -1
  119. package/src/infra/cache.ts +24 -11
  120. package/src/infra/freshness.ts +165 -0
  121. package/src/infra/market-calendar.ts +193 -0
  122. package/src/infra/rate-limiter.ts +1 -0
  123. package/src/market-state/summaries.ts +75 -0
  124. package/src/onboarding/providers.ts +29 -1
  125. package/src/pi/opencandle-extension.ts +55 -5
  126. package/src/providers/alpha-vantage.ts +6 -0
  127. package/src/providers/coingecko.ts +2 -0
  128. package/src/providers/polymarket.ts +214 -0
  129. package/src/providers/wrap-provider.ts +5 -3
  130. package/src/providers/yahoo-finance.ts +339 -27
  131. package/src/routing/entity-extractor.ts +17 -0
  132. package/src/routing/route-manifest.ts +1 -1
  133. package/src/routing/router-llm-client.ts +31 -19
  134. package/src/routing/router.ts +340 -5
  135. package/src/routing/slot-resolver.ts +1 -1
  136. package/src/runtime/evidence.ts +1 -0
  137. package/src/runtime/numeric-claims.ts +108 -0
  138. package/src/runtime/planning-evidence.ts +6 -91
  139. package/src/runtime/prompt-step.ts +127 -2
  140. package/src/runtime/session-coordinator.ts +462 -57
  141. package/src/runtime/validation.ts +1 -1
  142. package/src/runtime/workflow-events.ts +2 -0
  143. package/src/runtime/workflow-types.ts +3 -0
  144. package/src/tools/fundamentals/comps.ts +25 -14
  145. package/src/tools/fundamentals/dcf.ts +204 -124
  146. package/src/tools/index.ts +3 -0
  147. package/src/tools/macro/event-probabilities.ts +141 -0
  148. package/src/tools/market/crypto-price.ts +14 -2
  149. package/src/tools/market/screen-stocks.ts +14 -3
  150. package/src/tools/market/stock-quote.ts +49 -43
  151. package/src/tools/options/option-chain.ts +16 -2
  152. package/src/tools/portfolio/risk-analysis.ts +1 -1
  153. package/src/tools/technical/indicators.ts +1 -1
  154. package/src/types/fundamentals.ts +1 -0
  155. package/src/types/index.ts +1 -0
  156. package/src/types/market.ts +2 -0
  157. package/src/types/options.ts +1 -0
  158. package/src/types/prediction-markets.ts +13 -0
  159. package/gui/server/chat-run-session.ts +0 -16
  160. package/gui/web/dist/assets/CatalogOverlay-ChRTKNlf.js +0 -1
  161. package/gui/web/dist/assets/index-BzyqyVnd.css +0 -2
  162. package/gui/web/dist/assets/index-DvMjkOP9.js +0 -65
@@ -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 {
@@ -167,6 +168,31 @@ export function computeNetDebt(f: FinancialStatement): number | null {
167
168
  return null;
168
169
  }
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
+ }
194
+ }
195
+
170
196
  const params = Type.Object({
171
197
  symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT)" }),
172
198
  growth_rate: Type.Optional(
@@ -193,156 +219,210 @@ export const dcfTool: AgentTool<typeof params> = {
193
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.",
194
220
  parameters: params,
195
221
  async execute(_toolCallId, args) {
196
- return withCredentialCheck("alpha_vantage", async () => {
197
- const symbol = args.symbol.toUpperCase();
198
- const config = getConfig();
199
-
200
- const [overviewResult, financialsResult, quoteResult] = await Promise.all([
201
- wrapProvider("alphavantage", () => getOverview(symbol, config.alphaVantageApiKey!)),
202
- wrapProvider("alphavantage", () => getFinancials(symbol, config.alphaVantageApiKey!)),
203
- wrapProvider("yahoo", () => getQuote(symbol)),
204
- ]);
205
-
206
- const missing: string[] = [];
207
- if (overviewResult.status === "unavailable")
208
- missing.push(`company overview (${overviewResult.reason})`);
209
- if (financialsResult.status === "unavailable")
210
- missing.push(`financial statements (${financialsResult.reason})`);
211
- if (quoteResult.status === "unavailable") missing.push(`stock quote (${quoteResult.reason})`);
212
-
213
- if (financialsResult.status === "unavailable" || quoteResult.status === "unavailable") {
214
- return {
215
- content: [
216
- {
217
- type: "text",
218
- text: `⚠ DCF valuation unavailable for ${symbol}. Missing: ${missing.join(", ")}. Both financials and current price are required.`,
219
- },
220
- ],
221
- details: null,
222
- };
223
- }
222
+ const symbol = args.symbol.toUpperCase();
223
+ const config = getConfig();
224
+ const alphaVantageApiKey = config.alphaVantageApiKey;
224
225
 
225
- const overview = overviewResult.status === "ok" ? overviewResult.data : null;
226
- const financials = financialsResult.data;
227
- const quote = quoteResult.data;
226
+ const [alphaVantageFinancialsResult, quoteResult] = await Promise.all([
227
+ optionalAlphaVantage(alphaVantageApiKey, (apiKey) => getFinancials(symbol, apiKey)),
228
+ wrapProvider("yahoo", () => getQuote(symbol)),
229
+ ]);
228
230
 
229
- const latestFCF = financials[0]?.freeCashFlow ?? 0;
230
- if (latestFCF <= 0) {
231
- return {
232
- content: [
233
- {
234
- type: "text",
235
- text: `${symbol} has negative or zero free cash flow ($${latestFCF.toLocaleString()}). DCF is not meaningful for companies without positive FCF.`,
236
- },
237
- ],
238
- details: null,
239
- };
240
- }
231
+ const missing: string[] = [];
232
+ if (quoteResult.status === "unavailable") missing.push(`stock quote (${quoteResult.reason})`);
241
233
 
242
- // Estimate growth from historical FCF if not provided
243
- let growthRate = args.growth_rate ?? 0.1;
244
- if (!args.growth_rate && financials.length >= 2) {
245
- const olderFCF = financials[1]?.freeCashFlow;
246
- if (olderFCF && olderFCF > 0) {
247
- growthRate = Math.max(0.02, Math.min(0.25, (latestFCF - olderFCF) / olderFCF));
248
- }
249
- }
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
+ }
250
245
 
251
- const discountRate = args.discount_rate ?? 0.1;
252
- const terminalGrowth = args.terminal_growth ?? 0.03;
253
- if (discountRate <= terminalGrowth) {
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") {
254
274
  return {
255
275
  content: [
256
276
  {
257
277
  type: "text",
258
- 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.`,
278
+ text: `⚠ DCF valuation unavailable for ${symbol}. Missing: financial statements (Alpha Vantage: ${alphaVantageReason}; Yahoo Finance: ${yahooFinancialsResult.reason}). Both financials and current price are required.`,
259
279
  },
260
280
  ],
261
281
  details: null,
262
282
  };
263
283
  }
264
-
265
- const years = args.projection_years ?? 5;
266
- // Prefer the overview market cap; fall back to the quote's market cap.
267
- // Never substitute a placeholder share count — a fabricated
268
- // per-share value is worse than an honest refusal.
269
- const marketCap =
270
- overview?.marketCap && overview.marketCap > 0
271
- ? overview.marketCap
272
- : quote.marketCap > 0
273
- ? quote.marketCap
274
- : 0;
275
- if (quote.price <= 0 || marketCap <= 0) {
284
+ if (yahooFinancialsResult.stale) {
285
+ const cachedAt = yahooFinancialsResult.timestamp
286
+ ? ` from ${yahooFinancialsResult.timestamp}`
287
+ : "";
276
288
  return {
277
289
  content: [
278
290
  {
279
291
  type: "text",
280
- text: `⚠ Cannot compute a per-share DCF for ${symbol}: shares outstanding 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.`,
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.`,
281
293
  },
282
294
  ],
283
295
  details: null,
284
296
  };
285
297
  }
286
- const sharesOutstanding = marketCap / quote.price;
287
- // Signed on purpose: net cash (negative net debt) adds to equity value.
288
- // Omit the adjustment if debt/cash fields are missing; assets minus
289
- // liabilities is book equity, not a net-cash proxy.
290
- const computedNetDebt = financials[0] ? computeNetDebt(financials[0]) : null;
291
- const netDebt = computedNetDebt ?? 0;
292
-
293
- const result = computeDCF({
294
- freeCashFlow: latestFCF,
295
- growthRate,
296
- discountRate,
297
- terminalGrowth,
298
- years,
299
- netDebt,
300
- sharesOutstanding,
301
- });
302
- if (computedNetDebt == null) {
303
- result.warnings.push(
304
- "Net debt adjustment omitted because total debt and cash equivalents were unavailable.",
305
- );
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
+ }
314
+
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));
306
321
  }
322
+ }
307
323
 
308
- const marginOfSafety = (result.intrinsicValue - quote.price) / result.intrinsicValue;
309
- const upside = (result.intrinsicValue - quote.price) / quote.price;
310
-
311
- const lines = [
312
- `**${symbol} DCF Valuation**`,
313
- ``,
314
- `Current Price: $${quote.price.toFixed(2)}`,
315
- `Intrinsic Value: $${result.intrinsicValue.toFixed(2)}`,
316
- `Margin of Safety: ${(marginOfSafety * 100).toFixed(1)}%`,
317
- `Upside/Downside: ${upside >= 0 ? "+" : ""}${(upside * 100).toFixed(1)}%`,
318
- ``,
319
- `**Assumptions**`,
320
- `Free Cash Flow: $${(latestFCF / 1e9).toFixed(2)}B`,
321
- `Growth Rate: ${(growthRate * 100).toFixed(1)}%`,
322
- `Discount Rate (WACC): ${(discountRate * 100).toFixed(1)}%`,
323
- `Terminal Growth: ${(terminalGrowth * 100).toFixed(1)}%`,
324
- `Projection: ${years} years`,
325
- ``,
326
- `**Projected Cash Flows**`,
327
- ...result.projectedCashFlows.map(
328
- (cf) =>
329
- ` Year ${cf.year}: FCF $${(cf.fcf / 1e9).toFixed(2)}B → PV $${(cf.presentValue / 1e9).toFixed(2)}B`,
330
- ),
331
- ` Terminal Value: $${(result.terminalValue / 1e9).toFixed(2)}B`,
332
- ` Enterprise Value: $${(result.enterpriseValue / 1e9).toFixed(2)}B`,
333
- ``,
334
- `**Sensitivity Table** (Intrinsic Value at different Growth/Discount rates)`,
335
- ...formatSensitivityTable(result.sensitivityTable),
336
- ...(result.warnings.length > 0
337
- ? ["", "**Warnings**", ...result.warnings.map((warning) => `- ${warning}`)]
338
- : []),
339
- ];
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
+ }
340
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) {
341
357
  return {
342
- content: [{ type: "text", text: lines.join("\n") }],
343
- 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,
344
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,
345
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
+ };
346
426
  },
347
427
  };
348
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";
@@ -38,6 +39,7 @@ export { dcfTool } from "./fundamentals/dcf.js";
38
39
  export { earningsTool } from "./fundamentals/earnings.js";
39
40
  export { financialsTool } from "./fundamentals/financials.js";
40
41
  export { secFilingsTool } from "./fundamentals/sec-filings.js";
42
+ export { eventProbabilitiesTool } from "./macro/event-probabilities.js";
41
43
  export { fearGreedTool } from "./macro/fear-greed.js";
42
44
  export { fredDataTool } from "./macro/fred-data.js";
43
45
  export { cryptoHistoryTool } from "./market/crypto-history.js";
@@ -78,6 +80,7 @@ export function getAllTools(options: { askUserHandler?: AskUserHandler } = {}):
78
80
  dcfTool,
79
81
  compsTool,
80
82
  secFilingsTool,
83
+ eventProbabilitiesTool,
81
84
  fredDataTool,
82
85
  fearGreedTool,
83
86
  createRedditSentimentTool({ askUserHandler: options.askUserHandler }),
@@ -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
+ }