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 { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
4
4
  import { HttpError, httpGet } from "../infra/http-client.js";
5
5
  import { rateLimiter } from "../infra/rate-limiter.js";
6
6
  import { computeGreeks } from "../tools/options/greeks.js";
7
+ import type { CompanyOverview, FinancialStatement } from "../types/fundamentals.js";
7
8
  import type { OHLCV, StockQuote } from "../types/market.js";
8
9
  import type {
9
10
  OptionContract,
@@ -51,27 +52,107 @@ interface YahooChartResponse {
51
52
 
52
53
  interface YahooQuoteSummaryResponse {
53
54
  quoteSummary: {
54
- result?: Array<{
55
- price?: {
56
- symbol?: string;
57
- shortName?: string;
58
- longName?: string;
59
- };
60
- topHoldings?: {
61
- holdings?: Array<{
62
- symbol?: string;
63
- holdingName?: string;
64
- holdingPercent?: YahooNumber;
65
- }>;
66
- equityHoldings?: {
67
- sectorWeightings?: Array<Record<string, YahooNumber>>;
68
- };
69
- };
70
- }>;
55
+ result?: Array<YahooQuoteSummaryResult>;
71
56
  error?: { code?: string; description?: string } | null;
72
57
  };
73
58
  }
74
59
 
60
+ interface YahooQuoteSummaryResult {
61
+ price?: {
62
+ symbol?: string;
63
+ shortName?: string;
64
+ longName?: string;
65
+ exchangeName?: string;
66
+ marketCap?: YahooNumber;
67
+ };
68
+ assetProfile?: {
69
+ longBusinessSummary?: string;
70
+ sector?: string;
71
+ industry?: string;
72
+ };
73
+ summaryProfile?: {
74
+ longBusinessSummary?: string;
75
+ sector?: string;
76
+ industry?: string;
77
+ };
78
+ summaryDetail?: {
79
+ trailingPE?: YahooNumber;
80
+ forwardPE?: YahooNumber;
81
+ dividendYield?: YahooNumber;
82
+ beta?: YahooNumber;
83
+ fiftyTwoWeekHigh?: YahooNumber;
84
+ fiftyTwoWeekLow?: YahooNumber;
85
+ averageVolume?: YahooNumber;
86
+ };
87
+ defaultKeyStatistics?: {
88
+ trailingEps?: YahooNumber;
89
+ forwardEps?: YahooNumber;
90
+ beta?: YahooNumber;
91
+ };
92
+ financialData?: {
93
+ profitMargins?: YahooNumber;
94
+ revenueGrowth?: YahooNumber;
95
+ totalRevenue?: YahooNumber;
96
+ };
97
+ topHoldings?: {
98
+ holdings?: Array<{
99
+ symbol?: string;
100
+ holdingName?: string;
101
+ holdingPercent?: YahooNumber;
102
+ }>;
103
+ equityHoldings?: {
104
+ sectorWeightings?: Array<Record<string, YahooNumber>>;
105
+ };
106
+ };
107
+ }
108
+
109
+ export async function getYahooCompanyOverview(symbol: string): Promise<CompanyOverview> {
110
+ const normalizedSymbol = symbol.toUpperCase();
111
+ const cacheKey = `yahoo:overview:${normalizedSymbol}`;
112
+ const cached = cache.get<CompanyOverview>(cacheKey);
113
+ if (cached) return cached;
114
+
115
+ try {
116
+ await rateLimiter.acquire("yahoo");
117
+ const result = await getYahooCompanySummary(normalizedSymbol);
118
+ if (!result?.price?.symbol) {
119
+ throw new Error(`Yahoo Finance: no company fundamentals returned for ${normalizedSymbol}`);
120
+ }
121
+
122
+ const profile = result.assetProfile ?? result.summaryProfile;
123
+ const overview: CompanyOverview = {
124
+ symbol: result.price.symbol.toUpperCase(),
125
+ name: result.price.longName ?? result.price.shortName ?? result.price.symbol,
126
+ description: profile?.longBusinessSummary ?? "",
127
+ exchange: result.price.exchangeName ?? "",
128
+ sector: profile?.sector ?? "",
129
+ industry: profile?.industry ?? "",
130
+ marketCap: yahooNumber(result.price.marketCap) ?? 0,
131
+ pe: yahooNullableNumber(result.summaryDetail?.trailingPE),
132
+ forwardPe: yahooNullableNumber(result.summaryDetail?.forwardPE),
133
+ eps:
134
+ yahooNullableNumber(result.defaultKeyStatistics?.trailingEps) ??
135
+ yahooNullableNumber(result.defaultKeyStatistics?.forwardEps),
136
+ dividendYield: yahooNullableNumber(result.summaryDetail?.dividendYield),
137
+ beta:
138
+ yahooNullableNumber(result.summaryDetail?.beta) ??
139
+ yahooNullableNumber(result.defaultKeyStatistics?.beta),
140
+ week52High: yahooNumber(result.summaryDetail?.fiftyTwoWeekHigh) ?? 0,
141
+ week52Low: yahooNumber(result.summaryDetail?.fiftyTwoWeekLow) ?? 0,
142
+ avgVolume: yahooNumber(result.summaryDetail?.averageVolume) ?? 0,
143
+ profitMargin: yahooNullableNumber(result.financialData?.profitMargins),
144
+ revenueGrowth: yahooNullableNumber(result.financialData?.revenueGrowth),
145
+ };
146
+
147
+ cache.set(cacheKey, overview, TTL.FUNDAMENTALS);
148
+ return overview;
149
+ } catch (error) {
150
+ const stale = cache.getStale<CompanyOverview>(cacheKey, STALE_LIMIT.FUNDAMENTALS);
151
+ if (stale) return stale.value;
152
+ throw error;
153
+ }
154
+ }
155
+
75
156
  export async function getQuote(symbol: string): Promise<StockQuote> {
76
157
  const cacheKey = `yahoo:quote:${symbol}`;
77
158
  const cached = cache.get<StockQuote>(cacheKey);
@@ -117,6 +198,7 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
117
198
  week52High: meta.fiftyTwoWeekHigh ?? 0,
118
199
  week52Low: meta.fiftyTwoWeekLow ?? 0,
119
200
  timestamp: Date.now(),
201
+ asOf: yahooMarketTimeToIso(meta.regularMarketTime),
120
202
  currency:
121
203
  typeof meta.currency === "string" && meta.currency.trim() !== ""
122
204
  ? meta.currency.trim().toUpperCase()
@@ -136,6 +218,174 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
136
218
  }
137
219
  }
138
220
 
221
+ type YahooFundamentalsRow = {
222
+ date?: Date | string | number;
223
+ totalRevenue?: number;
224
+ annualTotalRevenue?: number;
225
+ grossProfit?: number;
226
+ annualGrossProfit?: number;
227
+ operatingIncome?: number;
228
+ annualOperatingIncome?: number;
229
+ netIncome?: number;
230
+ annualNetIncome?: number;
231
+ netIncomeCommonStockholders?: number;
232
+ annualNetIncomeCommonStockholders?: number;
233
+ basicEPS?: number;
234
+ annualBasicEPS?: number;
235
+ dilutedEPS?: number;
236
+ annualDilutedEPS?: number;
237
+ totalAssets?: number;
238
+ annualTotalAssets?: number;
239
+ totalLiabilitiesNetMinorityInterest?: number;
240
+ annualTotalLiabilitiesNetMinorityInterest?: number;
241
+ totalLiabilities?: number;
242
+ annualTotalLiabilities?: number;
243
+ stockholdersEquity?: number;
244
+ annualStockholdersEquity?: number;
245
+ commonStockEquity?: number;
246
+ annualCommonStockEquity?: number;
247
+ cashFlowFromContinuingOperatingActivities?: number;
248
+ annualCashFlowFromContinuingOperatingActivities?: number;
249
+ operatingCashFlow?: number;
250
+ annualOperatingCashFlow?: number;
251
+ freeCashFlow?: number;
252
+ annualFreeCashFlow?: number;
253
+ capitalExpenditure?: number;
254
+ annualCapitalExpenditure?: number;
255
+ capitalExpenditures?: number;
256
+ annualCapitalExpenditures?: number;
257
+ totalDebt?: number;
258
+ annualTotalDebt?: number;
259
+ cashAndCashEquivalents?: number;
260
+ annualCashAndCashEquivalents?: number;
261
+ ordinarySharesNumber?: number;
262
+ annualOrdinarySharesNumber?: number;
263
+ shareIssued?: number;
264
+ annualShareIssued?: number;
265
+ dilutedAverageShares?: number;
266
+ annualDilutedAverageShares?: number;
267
+ basicAverageShares?: number;
268
+ annualBasicAverageShares?: number;
269
+ };
270
+
271
+ export async function getYahooFinancials(symbol: string): Promise<FinancialStatement[]> {
272
+ const normalizedSymbol = symbol.toUpperCase();
273
+ const cacheKey = `yahoo:financials:${normalizedSymbol}`;
274
+ const cached = cache.get<FinancialStatement[]>(cacheKey);
275
+ if (cached) return cached;
276
+
277
+ try {
278
+ await rateLimiter.acquire("yahoo");
279
+
280
+ const period1 = new Date();
281
+ period1.setUTCFullYear(period1.getUTCFullYear() - 6);
282
+ const rows = (await getYahooFinance2Client().fundamentalsTimeSeries(
283
+ normalizedSymbol,
284
+ {
285
+ period1,
286
+ period2: new Date(),
287
+ type: "annual",
288
+ module: "all",
289
+ },
290
+ { validateResult: false },
291
+ )) as YahooFundamentalsRow[];
292
+
293
+ const statements = rows
294
+ .map((row) => yahooFundamentalsRowToStatement(row))
295
+ .filter((statement): statement is FinancialStatement => statement !== null)
296
+ .sort((a, b) => b.fiscalDate.localeCompare(a.fiscalDate))
297
+ .slice(0, 4);
298
+
299
+ if (statements.length === 0) {
300
+ throw new Error(`Yahoo Finance: no financial statements returned for ${normalizedSymbol}`);
301
+ }
302
+
303
+ cache.set(cacheKey, statements, TTL.FUNDAMENTALS);
304
+ return statements;
305
+ } catch (error) {
306
+ const stale = cache.getStale<FinancialStatement[]>(cacheKey, STALE_LIMIT.FUNDAMENTALS);
307
+ if (stale) return stale.value;
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ function yahooFundamentalsRowToStatement(row: YahooFundamentalsRow): FinancialStatement | null {
313
+ const fiscalDate = normalizeYahooDate(row.date);
314
+ if (!fiscalDate) return null;
315
+
316
+ const operatingCashFlowValue =
317
+ row.annualCashFlowFromContinuingOperatingActivities ??
318
+ row.cashFlowFromContinuingOperatingActivities ??
319
+ row.annualOperatingCashFlow ??
320
+ row.operatingCashFlow;
321
+ const capitalExpenditureValue =
322
+ row.annualCapitalExpenditure ??
323
+ row.capitalExpenditure ??
324
+ row.annualCapitalExpenditures ??
325
+ row.capitalExpenditures;
326
+ const operatingCashFlow = operatingCashFlowValue ?? 0;
327
+ const capitalExpenditure = capitalExpenditureValue ?? 0;
328
+ const freeCashFlow =
329
+ row.annualFreeCashFlow ??
330
+ row.freeCashFlow ??
331
+ (operatingCashFlowValue !== undefined && capitalExpenditureValue !== undefined
332
+ ? operatingCashFlow + capitalExpenditure
333
+ : 0);
334
+
335
+ return {
336
+ fiscalDate,
337
+ revenue: row.annualTotalRevenue ?? row.totalRevenue ?? 0,
338
+ grossProfit: row.annualGrossProfit ?? row.grossProfit ?? 0,
339
+ operatingIncome: row.annualOperatingIncome ?? row.operatingIncome ?? 0,
340
+ netIncome:
341
+ row.annualNetIncome ??
342
+ row.netIncome ??
343
+ row.annualNetIncomeCommonStockholders ??
344
+ row.netIncomeCommonStockholders ??
345
+ 0,
346
+ eps: row.annualDilutedEPS ?? row.dilutedEPS ?? row.annualBasicEPS ?? row.basicEPS ?? 0,
347
+ totalAssets: row.annualTotalAssets ?? row.totalAssets ?? 0,
348
+ totalLiabilities:
349
+ row.annualTotalLiabilitiesNetMinorityInterest ??
350
+ row.totalLiabilitiesNetMinorityInterest ??
351
+ row.annualTotalLiabilities ??
352
+ row.totalLiabilities ??
353
+ 0,
354
+ totalEquity:
355
+ row.annualStockholdersEquity ??
356
+ row.stockholdersEquity ??
357
+ row.annualCommonStockEquity ??
358
+ row.commonStockEquity ??
359
+ 0,
360
+ operatingCashFlow,
361
+ freeCashFlow,
362
+ totalDebt: row.annualTotalDebt ?? row.totalDebt,
363
+ cashAndEquivalents: row.annualCashAndCashEquivalents ?? row.cashAndCashEquivalents,
364
+ sharesOutstanding:
365
+ row.annualOrdinarySharesNumber ??
366
+ row.ordinarySharesNumber ??
367
+ row.annualShareIssued ??
368
+ row.shareIssued ??
369
+ row.annualDilutedAverageShares ??
370
+ row.dilutedAverageShares ??
371
+ row.annualBasicAverageShares ??
372
+ row.basicAverageShares,
373
+ };
374
+ }
375
+
376
+ function normalizeYahooDate(value: Date | string | number | undefined): string | null {
377
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
378
+ if (typeof value === "number") {
379
+ if (!Number.isFinite(value)) return null;
380
+ const timestampMs = Math.abs(value) < 1_000_000_000_000 ? value * 1000 : value;
381
+ const date = new Date(timestampMs);
382
+ return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);
383
+ }
384
+ if (typeof value !== "string" || value.trim() === "") return null;
385
+ const date = new Date(value);
386
+ return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);
387
+ }
388
+
139
389
  function isZeroResultQuote(quote: StockQuote): boolean {
140
390
  return (
141
391
  quote.price === 0 &&
@@ -243,28 +493,65 @@ export async function getFundHoldings(symbol: string): Promise<FundHoldings> {
243
493
  }
244
494
 
245
495
  async function getFundHoldingsSummary(symbol: string): Promise<YahooQuoteSummaryResponse> {
496
+ return getYahooQuoteSummary(symbol, "price,topHoldings");
497
+ }
498
+
499
+ async function getYahooCompanySummary(symbol: string): Promise<YahooQuoteSummaryResult> {
500
+ const modules = [
501
+ "price",
502
+ "assetProfile",
503
+ "summaryProfile",
504
+ "summaryDetail",
505
+ "defaultKeyStatistics",
506
+ "financialData",
507
+ ] as const;
246
508
  try {
247
- return await fetchFundHoldingsSummary(symbol);
509
+ return (await getYahooFinance2Client().quoteSummary(
510
+ symbol,
511
+ { modules: [...modules] },
512
+ { validateResult: false },
513
+ )) as YahooQuoteSummaryResult;
514
+ } catch (error) {
515
+ const data = await getYahooQuoteSummary(symbol, modules.join(","));
516
+ if (data.quoteSummary.error) {
517
+ throw new Error(
518
+ `Yahoo Finance: ${data.quoteSummary.error.description ?? data.quoteSummary.error.code ?? "quoteSummary error"}`,
519
+ );
520
+ }
521
+ return data.quoteSummary.result?.[0] ?? {};
522
+ }
523
+ }
524
+
525
+ async function getYahooQuoteSummary(
526
+ symbol: string,
527
+ modules: string,
528
+ ): Promise<YahooQuoteSummaryResponse> {
529
+ try {
530
+ return await fetchYahooQuoteSummary(symbol, modules);
248
531
  } catch (error) {
249
532
  if (!isYahooAuthError(error)) throw error;
250
- return fetchFundHoldingsSummaryWithCrumb(symbol);
533
+ return fetchYahooQuoteSummaryWithCrumb(symbol, modules);
251
534
  }
252
535
  }
253
536
 
254
- async function fetchFundHoldingsSummary(symbol: string): Promise<YahooQuoteSummaryResponse> {
255
- const modules = encodeURIComponent("price,topHoldings");
256
- const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}`;
537
+ async function fetchYahooQuoteSummary(
538
+ symbol: string,
539
+ modules: string,
540
+ ): Promise<YahooQuoteSummaryResponse> {
541
+ const encodedModules = encodeURIComponent(modules);
542
+ const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${encodedModules}`;
257
543
  return httpGet<YahooQuoteSummaryResponse>(url, {
258
544
  headers: { "User-Agent": "OpenCandle/1.0" },
259
545
  });
260
546
  }
261
547
 
262
- async function fetchFundHoldingsSummaryWithCrumb(
548
+ async function fetchYahooQuoteSummaryWithCrumb(
263
549
  symbol: string,
550
+ modules: string,
264
551
  ): Promise<YahooQuoteSummaryResponse> {
265
- const modules = encodeURIComponent("price,topHoldings");
552
+ const encodedModules = encodeURIComponent(modules);
266
553
  const { crumb, cookie } = await getYahooCrumb();
267
- const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}&crumb=${encodeURIComponent(crumb)}`;
554
+ const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${encodedModules}&crumb=${encodeURIComponent(crumb)}`;
268
555
  try {
269
556
  return await httpGet<YahooQuoteSummaryResponse>(url, {
270
557
  headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
@@ -273,7 +560,7 @@ async function fetchFundHoldingsSummaryWithCrumb(
273
560
  if (!isYahooAuthError(error)) throw error;
274
561
  clearCrumbCache();
275
562
  const fresh = await getYahooCrumb();
276
- const retryUrl = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}&crumb=${encodeURIComponent(fresh.crumb)}`;
563
+ const retryUrl = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${encodedModules}&crumb=${encodeURIComponent(fresh.crumb)}`;
277
564
  return httpGet<YahooQuoteSummaryResponse>(retryUrl, {
278
565
  headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
279
566
  });
@@ -285,11 +572,20 @@ function isYahooAuthError(error: unknown): boolean {
285
572
  }
286
573
 
287
574
  function normalizeHoldingWeight(value: YahooNumber | undefined): number | undefined {
288
- const numeric = typeof value === "number" ? value : value?.raw;
575
+ const numeric = yahooNumber(value);
289
576
  if (numeric === undefined || !Number.isFinite(numeric) || numeric <= 0) return undefined;
290
577
  return numeric > 1 ? roundWeight(numeric / 100) : roundWeight(numeric);
291
578
  }
292
579
 
580
+ function yahooNumber(value: YahooNumber | undefined): number | undefined {
581
+ const numeric = typeof value === "number" ? value : value?.raw;
582
+ return numeric !== undefined && Number.isFinite(numeric) ? numeric : undefined;
583
+ }
584
+
585
+ function yahooNullableNumber(value: YahooNumber | undefined): number | null {
586
+ return yahooNumber(value) ?? null;
587
+ }
588
+
293
589
  function normalizeSectorWeights(
294
590
  sectors: Array<Record<string, YahooNumber>> | undefined,
295
591
  ): Record<string, number> | undefined {
@@ -608,6 +904,7 @@ function parseOptionsResponse(data: YahooOptionsResponse): OptionsChain {
608
904
  putCallRatio: totalCallVolume > 0 ? totalPutVolume / totalCallVolume : 0,
609
905
  quoteStatus,
610
906
  fetchedAt: new Date().toISOString(),
907
+ asOf: yahooMarketTimeToIso(quote.regularMarketTime),
611
908
  };
612
909
  }
613
910
 
@@ -670,3 +967,18 @@ function toYahooUnixSeconds(value: Date | number | string): number {
670
967
  }
671
968
  return Math.floor(new Date(value).getTime() / 1000);
672
969
  }
970
+
971
+ function yahooMarketTimeToIso(value: unknown): string | undefined {
972
+ let date: Date | undefined;
973
+ if (value instanceof Date) {
974
+ date = value;
975
+ } else if (typeof value === "number" && Number.isFinite(value)) {
976
+ date = new Date(value < 10_000_000_000 ? value * 1000 : value);
977
+ } else if (typeof value === "string" && value.trim() !== "") {
978
+ const numeric = Number(value);
979
+ date = Number.isFinite(numeric)
980
+ ? new Date(numeric < 10_000_000_000 ? numeric * 1000 : numeric)
981
+ : new Date(value);
982
+ }
983
+ return date && Number.isFinite(date.getTime()) ? date.toISOString() : undefined;
984
+ }
@@ -189,6 +189,15 @@ const LOWERCASE_FINANCE_TERMS = new Set([
189
189
  "rate",
190
190
  "rates",
191
191
  "cuts",
192
+ // Trading verbs written in lowercase are actions, not tickers — "sell
193
+ // calls" must not mint a SELL symbol. Uppercase ticker extraction is
194
+ // unaffected, so a genuine $SELL / "SELL" ticker still resolves.
195
+ "sell",
196
+ "buy",
197
+ "hold",
198
+ "write",
199
+ "roll",
200
+ "trim",
192
201
  "gold",
193
202
  "oil",
194
203
  "stock",
@@ -478,6 +487,14 @@ function extractDteHint(input: string): string | undefined {
478
487
  const lower = input.toLowerCase();
479
488
  const explicitDays = lower.match(/\b(\d+)\s*(?:-|to|or)\s*(\d+)\s*(?:dte|days?)\b/);
480
489
  if (explicitDays) return `${explicitDays[1]}-${explicitDays[2]} days`;
490
+ // An explicit user week range ("1-2 weeks out") must outrank the
491
+ // catalyst-derived event_week inference below; a same-day earnings
492
+ // mention otherwise collapses the requested horizon to 0-7 days
493
+ // (historical "1-2 weeks DTE preservation" loss class).
494
+ const explicitWeeks = lower.match(
495
+ /\b(\d+|one|two|three|four)\s*(?:[-–]|to|or)\s*(\d+|one|two|three|four)\s*weeks?\b/,
496
+ );
497
+ if (explicitWeeks) return `${explicitWeeks[1]}-${explicitWeeks[2]} weeks`;
481
498
  if (/\bmonth\b/.test(lower)) return "month";
482
499
  if (
483
500
  /\bearnings?\b.*\b(?:today|tonight|this\s+week)\b|\b(?:today|tonight|this\s+week)\b.*\bearnings?\b/.test(
@@ -35,7 +35,7 @@ export const TOOL_BUNDLE_TOOLS: Record<ToolBundleName, readonly string[]> = {
35
35
  "search_web",
36
36
  ],
37
37
  options: ["get_option_chain", "get_stock_quote", "search_ticker", "search_web"],
38
- macro: ["get_economic_data", "get_fear_greed", "search_web"],
38
+ macro: ["get_economic_data", "get_event_probabilities", "get_fear_greed", "search_web"],
39
39
  sentiment: [
40
40
  "get_reddit_sentiment",
41
41
  "get_twitter_sentiment",
@@ -2,6 +2,8 @@ import type { Model } from "@earendil-works/pi-ai";
2
2
  import { completeSimple } from "@earendil-works/pi-ai/compat";
3
3
  import type { RouterLlmClient } from "./router-types.js";
4
4
 
5
+ type CompleteSimpleResponse = Awaited<ReturnType<typeof completeSimple>>;
6
+
5
7
  /**
6
8
  * Build a router LLM client backed by pi-ai's `completeSimple`. The client
7
9
  * is intentionally thin: prompt in, raw text out. Schema validation and
@@ -15,25 +17,30 @@ export function createPiAiRouterClient(
15
17
  ): RouterLlmClient {
16
18
  return {
17
19
  async complete(prompt: string): Promise<string> {
18
- const response = await completeSimple(
19
- model,
20
- {
21
- messages: [
22
- {
23
- role: "user",
24
- content: prompt,
25
- timestamp: Date.now(),
26
- },
27
- ],
28
- // Explicitly no tools — spec requirement.
29
- tools: [],
30
- },
31
- {
32
- temperature: 0,
33
- maxTokens: 2000,
34
- reasoning: "minimal",
35
- },
36
- );
20
+ const request = {
21
+ messages: [
22
+ {
23
+ role: "user" as const,
24
+ content: prompt,
25
+ timestamp: Date.now(),
26
+ },
27
+ ],
28
+ // Explicitly no tools — spec requirement.
29
+ tools: [],
30
+ };
31
+ const options = {
32
+ temperature: 0,
33
+ maxTokens: 2000,
34
+ reasoning: "minimal" as const,
35
+ };
36
+ let response: CompleteSimpleResponse;
37
+ try {
38
+ response = await completeSimple(model, request, options);
39
+ } catch (error) {
40
+ if (!isUnsupportedTemperatureError(error)) throw error;
41
+ const { temperature: _temperature, ...retryOptions } = options;
42
+ response = await completeSimple(model, request, retryOptions);
43
+ }
37
44
 
38
45
  if (response.stopReason === "error" || response.stopReason === "aborted") {
39
46
  throw new Error(`router LLM call failed: ${response.errorMessage ?? response.stopReason}`);
@@ -50,3 +57,8 @@ export function createPiAiRouterClient(
50
57
  },
51
58
  };
52
59
  }
60
+
61
+ function isUnsupportedTemperatureError(error: unknown): boolean {
62
+ const message = error instanceof Error ? error.message : String(error);
63
+ return /unsupported parameter/i.test(message) && /temperature/i.test(message);
64
+ }