opencandle 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -14
  3. package/dist/cli.js +2 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +19 -3
  6. package/dist/config.js +61 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/infra/browser.d.ts +1 -3
  9. package/dist/infra/browser.js +1 -1
  10. package/dist/infra/browser.js.map +1 -1
  11. package/dist/infra/rate-limiter.d.ts +4 -0
  12. package/dist/infra/rate-limiter.js +5 -1
  13. package/dist/infra/rate-limiter.js.map +1 -1
  14. package/dist/memory/manager.d.ts +9 -0
  15. package/dist/memory/manager.js +28 -11
  16. package/dist/memory/manager.js.map +1 -1
  17. package/dist/memory/storage.d.ts +3 -2
  18. package/dist/memory/storage.js.map +1 -1
  19. package/dist/memory/types.js +4 -0
  20. package/dist/memory/types.js.map +1 -1
  21. package/dist/pi/opencandle-extension.js +230 -36
  22. package/dist/pi/opencandle-extension.js.map +1 -1
  23. package/dist/pi/setup.js +10 -0
  24. package/dist/pi/setup.js.map +1 -1
  25. package/dist/prompts/context-builder.d.ts +18 -3
  26. package/dist/prompts/context-builder.js +102 -16
  27. package/dist/prompts/context-builder.js.map +1 -1
  28. package/dist/prompts/disclaimer.js +1 -1
  29. package/dist/prompts/disclaimer.js.map +1 -1
  30. package/dist/prompts/policy-cards.d.ts +13 -0
  31. package/dist/prompts/policy-cards.js +197 -0
  32. package/dist/prompts/policy-cards.js.map +1 -0
  33. package/dist/prompts/sections.js +3 -3
  34. package/dist/prompts/sections.js.map +1 -1
  35. package/dist/prompts/workflow-prompts.js +170 -18
  36. package/dist/prompts/workflow-prompts.js.map +1 -1
  37. package/dist/providers/alpha-vantage.js +23 -1
  38. package/dist/providers/alpha-vantage.js.map +1 -1
  39. package/dist/providers/sec-edgar.d.ts +8 -1
  40. package/dist/providers/sec-edgar.js +172 -5
  41. package/dist/providers/sec-edgar.js.map +1 -1
  42. package/dist/providers/yahoo-finance.d.ts +2 -0
  43. package/dist/providers/yahoo-finance.js +134 -3
  44. package/dist/providers/yahoo-finance.js.map +1 -1
  45. package/dist/routing/classify-intent.d.ts +3 -0
  46. package/dist/routing/classify-intent.js +82 -3
  47. package/dist/routing/classify-intent.js.map +1 -1
  48. package/dist/routing/defaults.js +3 -3
  49. package/dist/routing/defaults.js.map +1 -1
  50. package/dist/routing/entity-extractor.d.ts +1 -0
  51. package/dist/routing/entity-extractor.js +158 -12
  52. package/dist/routing/entity-extractor.js.map +1 -1
  53. package/dist/routing/index.d.ts +7 -1
  54. package/dist/routing/index.js +4 -0
  55. package/dist/routing/index.js.map +1 -1
  56. package/dist/routing/legacy-rule-router.d.ts +9 -0
  57. package/dist/routing/legacy-rule-router.js +12 -0
  58. package/dist/routing/legacy-rule-router.js.map +1 -0
  59. package/dist/routing/planning.d.ts +54 -0
  60. package/dist/routing/planning.js +531 -0
  61. package/dist/routing/planning.js.map +1 -0
  62. package/dist/routing/route-manifest.d.ts +35 -0
  63. package/dist/routing/route-manifest.js +221 -0
  64. package/dist/routing/route-manifest.js.map +1 -0
  65. package/dist/routing/router-prompt.js +45 -42
  66. package/dist/routing/router-prompt.js.map +1 -1
  67. package/dist/routing/router-types.d.ts +9 -0
  68. package/dist/routing/router.d.ts +1 -0
  69. package/dist/routing/router.js +456 -12
  70. package/dist/routing/router.js.map +1 -1
  71. package/dist/routing/slot-resolver.js +46 -6
  72. package/dist/routing/slot-resolver.js.map +1 -1
  73. package/dist/routing/turn-context.d.ts +44 -0
  74. package/dist/routing/turn-context.js +45 -0
  75. package/dist/routing/turn-context.js.map +1 -0
  76. package/dist/routing/types.d.ts +13 -1
  77. package/dist/runtime/answer-contracts.d.ts +82 -0
  78. package/dist/runtime/answer-contracts.js +414 -0
  79. package/dist/runtime/answer-contracts.js.map +1 -0
  80. package/dist/runtime/artifact-contracts.d.ts +14 -0
  81. package/dist/runtime/artifact-contracts.js +57 -0
  82. package/dist/runtime/artifact-contracts.js.map +1 -0
  83. package/dist/runtime/planning-evidence.d.ts +99 -0
  84. package/dist/runtime/planning-evidence.js +445 -0
  85. package/dist/runtime/planning-evidence.js.map +1 -0
  86. package/dist/runtime/session-coordinator.d.ts +20 -2
  87. package/dist/runtime/session-coordinator.js +47 -14
  88. package/dist/runtime/session-coordinator.js.map +1 -1
  89. package/dist/system-prompt.js +4 -1
  90. package/dist/system-prompt.js.map +1 -1
  91. package/dist/tools/fundamentals/company-overview.js +1 -1
  92. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  93. package/dist/tools/fundamentals/comps.js +1 -1
  94. package/dist/tools/fundamentals/comps.js.map +1 -1
  95. package/dist/tools/fundamentals/dcf.js +1 -1
  96. package/dist/tools/fundamentals/dcf.js.map +1 -1
  97. package/dist/tools/fundamentals/earnings.js +1 -1
  98. package/dist/tools/fundamentals/earnings.js.map +1 -1
  99. package/dist/tools/fundamentals/financials.js +1 -1
  100. package/dist/tools/fundamentals/financials.js.map +1 -1
  101. package/dist/tools/fundamentals/sec-filings.d.ts +1 -0
  102. package/dist/tools/fundamentals/sec-filings.js +19 -2
  103. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  104. package/dist/tools/index.d.ts +1 -0
  105. package/dist/tools/index.js +3 -0
  106. package/dist/tools/index.js.map +1 -1
  107. package/dist/tools/macro/fear-greed.js +1 -1
  108. package/dist/tools/macro/fear-greed.js.map +1 -1
  109. package/dist/tools/macro/fred-data.js +29 -5
  110. package/dist/tools/macro/fred-data.js.map +1 -1
  111. package/dist/tools/market/crypto-history.js +18 -2
  112. package/dist/tools/market/crypto-history.js.map +1 -1
  113. package/dist/tools/market/crypto-price.js +1 -1
  114. package/dist/tools/market/crypto-price.js.map +1 -1
  115. package/dist/tools/market/search-ticker.js +1 -1
  116. package/dist/tools/market/search-ticker.js.map +1 -1
  117. package/dist/tools/market/stock-history.js +1 -1
  118. package/dist/tools/market/stock-history.js.map +1 -1
  119. package/dist/tools/market/stock-quote.js +1 -1
  120. package/dist/tools/market/stock-quote.js.map +1 -1
  121. package/dist/tools/options/greeks.js +0 -1
  122. package/dist/tools/options/greeks.js.map +1 -1
  123. package/dist/tools/options/option-chain.js +9 -4
  124. package/dist/tools/options/option-chain.js.map +1 -1
  125. package/dist/tools/portfolio/correlation.js +1 -1
  126. package/dist/tools/portfolio/correlation.js.map +1 -1
  127. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  128. package/dist/tools/portfolio/holdings-overlap.js +105 -0
  129. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  130. package/dist/tools/portfolio/predictions.js +1 -1
  131. package/dist/tools/portfolio/predictions.js.map +1 -1
  132. package/dist/tools/portfolio/risk-analysis.js +1 -1
  133. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  134. package/dist/tools/portfolio/tracker.js +1 -1
  135. package/dist/tools/portfolio/tracker.js.map +1 -1
  136. package/dist/tools/portfolio/watchlist.js +12 -4
  137. package/dist/tools/portfolio/watchlist.js.map +1 -1
  138. package/dist/tools/sentiment/reddit-sentiment.js +1 -1
  139. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  140. package/dist/tools/sentiment/sentiment-summary.js +57 -2
  141. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  142. package/dist/tools/sentiment/twitter-sentiment.js +1 -1
  143. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  144. package/dist/tools/sentiment/web-search.js +32 -3
  145. package/dist/tools/sentiment/web-search.js.map +1 -1
  146. package/dist/tools/sentiment/web-sentiment.js +1 -1
  147. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  148. package/dist/tools/technical/backtest.d.ts +2 -2
  149. package/dist/tools/technical/backtest.js +41 -27
  150. package/dist/tools/technical/backtest.js.map +1 -1
  151. package/dist/tools/technical/indicators.js +1 -3
  152. package/dist/tools/technical/indicators.js.map +1 -1
  153. package/dist/types/options.d.ts +10 -0
  154. package/dist/types/portfolio.d.ts +27 -0
  155. package/dist/workflows/compare-assets.js +38 -2
  156. package/dist/workflows/compare-assets.js.map +1 -1
  157. package/dist/workflows/options-screener.js +88 -7
  158. package/dist/workflows/options-screener.js.map +1 -1
  159. package/dist/workflows/portfolio-builder.js +7 -3
  160. package/dist/workflows/portfolio-builder.js.map +1 -1
  161. package/gui/server/ask-user-bridge.ts +82 -0
  162. package/gui/server/gui-session-manager.ts +5 -0
  163. package/gui/server/projector.ts +47 -5
  164. package/gui/server/prompt-observation.ts +61 -0
  165. package/gui/server/server.ts +119 -8
  166. package/gui/server/session-entry-wait.ts +81 -0
  167. package/gui/web/dist/assets/{CatalogOverlay-D1ImSJTe.js → CatalogOverlay-Bmp6Knu7.js} +1 -1
  168. package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
  169. package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
  170. package/gui/web/dist/index.html +2 -2
  171. package/package.json +18 -12
  172. package/src/cli.ts +2 -1
  173. package/src/config.ts +89 -5
  174. package/src/infra/browser.ts +1 -1
  175. package/src/infra/rate-limiter.ts +10 -1
  176. package/src/memory/manager.ts +43 -10
  177. package/src/memory/storage.ts +3 -2
  178. package/src/memory/types.ts +4 -0
  179. package/src/pi/opencandle-extension.ts +273 -42
  180. package/src/pi/setup.ts +10 -0
  181. package/src/prompts/context-builder.ts +128 -17
  182. package/src/prompts/disclaimer.ts +1 -1
  183. package/src/prompts/policy-cards.ts +220 -0
  184. package/src/prompts/sections.ts +3 -3
  185. package/src/prompts/workflow-prompts.ts +172 -18
  186. package/src/providers/alpha-vantage.ts +24 -1
  187. package/src/providers/sec-edgar.ts +220 -4
  188. package/src/providers/web-search.ts +1 -1
  189. package/src/providers/yahoo-finance.ts +171 -4
  190. package/src/routing/classify-intent.ts +94 -3
  191. package/src/routing/defaults.ts +3 -3
  192. package/src/routing/entity-extractor.ts +164 -13
  193. package/src/routing/index.ts +44 -0
  194. package/src/routing/legacy-rule-router.ts +13 -0
  195. package/src/routing/planning.ts +732 -0
  196. package/src/routing/route-manifest.ts +287 -0
  197. package/src/routing/router-prompt.ts +50 -46
  198. package/src/routing/router-types.ts +21 -0
  199. package/src/routing/router.ts +511 -12
  200. package/src/routing/slot-resolver.ts +44 -6
  201. package/src/routing/turn-context.ts +111 -0
  202. package/src/routing/types.ts +13 -1
  203. package/src/runtime/answer-contracts.ts +633 -0
  204. package/src/runtime/artifact-contracts.ts +76 -0
  205. package/src/runtime/planning-evidence.ts +591 -0
  206. package/src/runtime/session-coordinator.ts +78 -12
  207. package/src/system-prompt.ts +4 -1
  208. package/src/tools/fundamentals/company-overview.ts +1 -1
  209. package/src/tools/fundamentals/comps.ts +1 -1
  210. package/src/tools/fundamentals/dcf.ts +1 -1
  211. package/src/tools/fundamentals/earnings.ts +1 -1
  212. package/src/tools/fundamentals/financials.ts +1 -1
  213. package/src/tools/fundamentals/sec-filings.ts +25 -2
  214. package/src/tools/index.ts +3 -0
  215. package/src/tools/macro/fear-greed.ts +1 -1
  216. package/src/tools/macro/fred-data.ts +31 -5
  217. package/src/tools/market/crypto-history.ts +18 -2
  218. package/src/tools/market/crypto-price.ts +1 -1
  219. package/src/tools/market/search-ticker.ts +1 -1
  220. package/src/tools/market/stock-history.ts +1 -1
  221. package/src/tools/market/stock-quote.ts +1 -1
  222. package/src/tools/options/greeks.ts +0 -1
  223. package/src/tools/options/option-chain.ts +9 -4
  224. package/src/tools/portfolio/correlation.ts +1 -1
  225. package/src/tools/portfolio/holdings-overlap.ts +123 -0
  226. package/src/tools/portfolio/predictions.ts +1 -1
  227. package/src/tools/portfolio/risk-analysis.ts +1 -1
  228. package/src/tools/portfolio/tracker.ts +1 -1
  229. package/src/tools/portfolio/watchlist.ts +10 -4
  230. package/src/tools/sentiment/reddit-sentiment.ts +1 -1
  231. package/src/tools/sentiment/sentiment-summary.ts +62 -2
  232. package/src/tools/sentiment/twitter-sentiment.ts +1 -1
  233. package/src/tools/sentiment/web-search.ts +36 -3
  234. package/src/tools/sentiment/web-sentiment.ts +1 -1
  235. package/src/tools/technical/backtest.ts +50 -29
  236. package/src/tools/technical/indicators.ts +1 -3
  237. package/src/types/options.ts +17 -0
  238. package/src/types/portfolio.ts +32 -0
  239. package/src/workflows/compare-assets.ts +38 -2
  240. package/src/workflows/options-screener.ts +85 -7
  241. package/src/workflows/portfolio-builder.ts +7 -3
  242. package/dist/runtime/index.d.ts +0 -16
  243. package/dist/runtime/index.js +0 -10
  244. package/dist/runtime/index.js.map +0 -1
  245. package/dist/runtime/provider-ids.d.ts +0 -14
  246. package/dist/runtime/provider-ids.js +0 -14
  247. package/dist/runtime/provider-ids.js.map +0 -1
  248. package/gui/web/dist/assets/index-DBrWq43L.css +0 -1
  249. package/gui/web/dist/assets/index-RflHaj0y.js +0 -67
  250. package/src/runtime/index.ts +0 -55
  251. package/src/runtime/provider-ids.ts +0 -15
@@ -19,6 +19,10 @@ import { PromptContextBuilder, type FallbackContext } from "../prompts/context-b
19
19
  import { getAddonToolDescriptions } from "../tool-kit.js";
20
20
  import type { WorkflowDefinition } from "./prompt-step.js";
21
21
  import { toStepDefinitions, promptStepOutput } from "./prompt-step.js";
22
+ import type { ResolvedTurnContext } from "../routing/turn-context.js";
23
+ import type { RouterRouteKind } from "../routing/router-types.js";
24
+ import type { MemoryEntry } from "../memory/types.js";
25
+ import type { FilteredMemoryEntry } from "../memory/manager.js";
22
26
  import type Database from "better-sqlite3";
23
27
 
24
28
  const PROMPT_SETTLE_POLL_MS = 25;
@@ -57,6 +61,7 @@ function sleep(ms: number): Promise<void> {
57
61
  async function waitForPromptSettlement(
58
62
  ctx: QueueContext,
59
63
  isCurrentRun: () => boolean,
64
+ options: { requireActivity?: boolean } = {},
60
65
  ): Promise<boolean> {
61
66
  let sawBusyOrPending = !isReadyForNextPrompt(ctx);
62
67
  const startedAt = Date.now();
@@ -71,7 +76,12 @@ async function waitForPromptSettlement(
71
76
  return true;
72
77
  }
73
78
 
74
- if (!sawBusyOrPending && ready && Date.now() - startedAt >= IMMEDIATE_IDLE_GRACE_MS) {
79
+ if (
80
+ !options.requireActivity &&
81
+ !sawBusyOrPending &&
82
+ ready &&
83
+ Date.now() - startedAt >= IMMEDIATE_IDLE_GRACE_MS
84
+ ) {
75
85
  return true;
76
86
  }
77
87
 
@@ -150,7 +160,7 @@ export class SessionCoordinator {
150
160
  entities: object,
151
161
  resolved: object,
152
162
  defaultsUsed: unknown[],
153
- turnType: "workflow" | "fallback" = "workflow",
163
+ turnType = "workflow",
154
164
  ): void {
155
165
  this.storage?.insertWorkflowRun({
156
166
  sessionId: this.sessionId,
@@ -263,11 +273,24 @@ export class SessionCoordinator {
263
273
  return { profileSnapshot, recentWorkflowRuns: runs, priorTurns };
264
274
  }
265
275
 
276
+ retrieveMemoryForRoute(
277
+ routeKind: RouterRouteKind,
278
+ workflowType?: string,
279
+ overriddenSlots?: string[],
280
+ ): { entries: MemoryEntry[]; filtered: FilteredMemoryEntry[] } {
281
+ if (!this.memoryManager) return { entries: [], filtered: [] };
282
+ return this.memoryManager.retrieveDetailed(
283
+ workflowType ?? routeKind,
284
+ overriddenSlots,
285
+ );
286
+ }
287
+
266
288
  /** Build system prompt using composable sections. */
267
289
  buildSystemPrompt(
268
290
  basePrompt: string,
269
291
  workflowType?: string,
270
292
  fallbackContext?: FallbackContext,
293
+ resolvedTurnContext?: ResolvedTurnContext,
271
294
  ): string {
272
295
  const builder = new PromptContextBuilder();
273
296
 
@@ -277,7 +300,9 @@ export class SessionCoordinator {
277
300
  : undefined;
278
301
 
279
302
  const memoryContext = this.memoryManager
280
- ? this.memoryManager.buildContext(workflowType ?? "unclassified")
303
+ ? this.memoryManager.buildContext(
304
+ resolvedTurnContext?.workflow ?? workflowType ?? resolvedTurnContext?.routeKind ?? "unclassified",
305
+ )
281
306
  : undefined;
282
307
 
283
308
  builder.populateFromOptions({
@@ -285,6 +310,7 @@ export class SessionCoordinator {
285
310
  memoryContext: memoryContext || undefined,
286
311
  addonToolDescriptions: addonDescriptions,
287
312
  fallbackContext,
313
+ resolvedTurnContext,
288
314
  });
289
315
 
290
316
  const toolDefaults = formatToolDefaultsForPrompt();
@@ -301,17 +327,28 @@ export class SessionCoordinator {
301
327
  * subsequent turns do not inherit stale fallback directives.
302
328
  */
303
329
  private pendingFallbackContext: FallbackContext | null = null;
330
+ private pendingResolvedTurnContext: ResolvedTurnContext | null = null;
304
331
 
305
332
  setPendingFallbackContext(ctx: FallbackContext | null): void {
306
333
  this.pendingFallbackContext = ctx;
307
334
  }
308
335
 
336
+ setPendingResolvedTurnContext(ctx: ResolvedTurnContext | null): void {
337
+ this.pendingResolvedTurnContext = ctx;
338
+ }
339
+
309
340
  consumePendingFallbackContext(): FallbackContext | null {
310
341
  const ctx = this.pendingFallbackContext;
311
342
  this.pendingFallbackContext = null;
312
343
  return ctx;
313
344
  }
314
345
 
346
+ consumePendingResolvedTurnContext(): ResolvedTurnContext | null {
347
+ const ctx = this.pendingResolvedTurnContext;
348
+ this.pendingResolvedTurnContext = null;
349
+ return ctx;
350
+ }
351
+
315
352
  /**
316
353
  * Execute a workflow definition through the WorkflowRunner,
317
354
  * sending prompts via Pi with settlement-based sequencing.
@@ -320,21 +357,46 @@ export class SessionCoordinator {
320
357
  pi: ExtensionAPI,
321
358
  definition: WorkflowDefinition,
322
359
  ctx: QueueContext,
360
+ ): void {
361
+ this.startWorkflowRun(pi, definition, ctx, "send");
362
+ }
363
+
364
+ /**
365
+ * Start workflow tracking for an input handler that will return a Pi
366
+ * transform result. The current prompt becomes the first workflow prompt;
367
+ * only later steps are sent through Pi.
368
+ */
369
+ transformWorkflowInput(
370
+ pi: ExtensionAPI,
371
+ definition: WorkflowDefinition,
372
+ ctx: QueueContext,
373
+ ): string | undefined {
374
+ if (definition.steps.length === 0) return undefined;
375
+ this.startWorkflowRun(pi, definition, ctx, "transform");
376
+ return definition.steps[0].prompt;
377
+ }
378
+
379
+ private startWorkflowRun(
380
+ pi: ExtensionAPI,
381
+ definition: WorkflowDefinition,
382
+ ctx: QueueContext,
383
+ firstPromptMode: "send" | "transform",
323
384
  ): void {
324
385
  if (definition.steps.length === 0) return;
325
386
 
326
387
  const runner = this.runner;
327
388
  const runRef = { active: true };
328
389
 
329
- // Send the first prompt immediately
330
- const [firstStep, ...restSteps] = definition.steps;
331
- const startedBusy = !isReadyForNextPrompt(ctx);
390
+ const [firstStep] = definition.steps;
332
391
 
333
- if (startedBusy) {
334
- pi.sendUserMessage(firstStep.prompt, { deliverAs: "followUp" });
335
- ctx.ui?.notify?.("Analysis queued as follow-up.", "info");
336
- } else {
337
- pi.sendUserMessage(firstStep.prompt);
392
+ if (firstPromptMode === "send") {
393
+ const startedBusy = !isReadyForNextPrompt(ctx);
394
+ if (startedBusy) {
395
+ pi.sendUserMessage(firstStep.prompt, { deliverAs: "followUp" });
396
+ ctx.ui?.notify?.("Analysis queued as follow-up.", "info");
397
+ } else {
398
+ pi.sendUserMessage(firstStep.prompt);
399
+ }
338
400
  }
339
401
 
340
402
  // Make the run's ProviderTracker accessible to tools during execution
@@ -352,7 +414,11 @@ export class SessionCoordinator {
352
414
  pi.sendUserMessage(definition.steps[stepIndex].prompt);
353
415
  } else {
354
416
  // For the first step, just wait for it to settle
355
- const settled = await waitForPromptSettlement(ctx, () => runRef.active);
417
+ const settled = await waitForPromptSettlement(
418
+ ctx,
419
+ () => runRef.active,
420
+ { requireActivity: firstPromptMode === "transform" },
421
+ );
356
422
  if (!settled || !runRef.active) {
357
423
  throw new Error("run_cancelled");
358
424
  }
@@ -10,7 +10,7 @@ ${memoryContext}`
10
10
  return `You are OpenCandle, a research analyst for investors and traders.
11
11
 
12
12
  ## Your Role
13
- You are an analyst, not a fiduciary advisor. When asked for entry levels, price targets, stops, position sizes, or allocations, you COMMIT to specific numbers backed by the data you fetched. Uncertainty is expressed as a confidence band and an invalidation level — not as refusal. Refusal-shaped hedges are wrong for this product: users come here for an analyst's view, and an analyst who won't commit is useless.
13
+ You are an analyst, not a fiduciary advisor. When asked for entry levels, price targets, stops, position sizes, or allocations, you COMMIT to specific numbers backed by the data you fetched. Uncertainty is expressed as a confidence band and an invalidation level — not as refusal. Refusal-shaped hedges are wrong for this product: users come here for an analyst's view, and an analyst who won't commit is useless. For conceptual education questions, teach the concept directly, do not name tool functions, and do not append analyst-view, confidence-band, or invalidation boilerplate. For valuation-metric education, start with "Bottom line", use a heading exactly named "Practical workflow" with numbered question-driven application steps, explain where the metric misleads, include a compact cross-check table with why/when each metric helps, include relevant trailing, forward, normalized, or cyclically adjusted variants when useful, and end with a heading exactly named "Quick checklist".
14
14
 
15
15
  ## Available Tools
16
16
  - **Market Data**: get_stock_quote, get_stock_history, get_crypto_price, get_crypto_history — real-time and historical price data
@@ -45,11 +45,14 @@ Calibrate explanation depth from conversational signals: the user's vocabulary i
45
45
 
46
46
  ## Guidelines
47
47
  - Always fetch data with tools before stating prices, ratios, or metrics. Never guess financial numbers. Every substantive response should be backed by at least one tool call — if you find yourself writing a response with zero tool calls, stop and think about what data would make it better.
48
+ - For current single-stock recommendations, state the quote or tool-output date in the final answer. If tool output says the market is closed, the quote is delayed, or this is the last available quote, carry that freshness note into the final answer. If DCF or another valuation model is unavailable or not meaningful, do not let that tool failure become the whole valuation view; use supported fallback valuation lenses such as relative multiples, growth-adjusted multiples, cash-flow quality, balance-sheet risk, and historical range context. Do not make missing fundamentals the main thesis when quote, earnings, technicals, sentiment, or news are available; use those data points plus structural business risks to give a clear call, position sizing, and entry strategy.
49
+ - For ticker-specific sentiment prompts, call get_stock_quote before the final answer and state whether sentiment diverges from price action. For sentiment-only prompts, include source-coverage risk, low sample counts, missing sources, and how those gaps downgrade confidence.
48
50
  - For options analysis, use get_option_chain to see the full chain with Greeks. Pay attention to put/call ratio, unusual volume, and IV levels.
49
51
  - Present numerical data in tables when comparing multiple securities.
50
52
  - Include data timestamps so users know how fresh the information is.
51
53
  - Be concise and actionable. Lead with the commitment, then the reasoning chain.
52
54
  - Flag downside and risks loudly. Commitment is not optimism — a bearish analyst view with conviction is valid output. Risk is expressed through the invalidation level and confidence band, never through refusal.
55
+ - Conceptual education prompts are not committal responses. Do not append "Analyst View", "Commitment", "Reasoning Chain", "Confidence Band", or "Invalidation Level" sections when the user asked for an explanation, definition, or learning framework rather than a trade, allocation, or recommendation.
53
56
  - Reuse prior tool outputs when they already answer the question. Do not re-fetch the same symbol and parameters unless you need a missing field or fresher timestamp.
54
57
  - If one provider is missing data, continue with the remaining tools and clearly label unavailable metrics instead of aborting the entire response.
55
58
 
@@ -16,7 +16,7 @@ export const companyOverviewTool: AgentTool<typeof params, CompanyOverview | { c
16
16
  description:
17
17
  "Get company fundamentals: P/E ratio, EPS, market cap, sector, dividend yield, profit margin, beta, and description. Requires Alpha Vantage.",
18
18
  parameters: params,
19
- async execute(toolCallId, args) {
19
+ async execute(_toolCallId, args) {
20
20
  return withCredentialCheck("alpha_vantage", async () => {
21
21
  const apiKey = getConfig().alphaVantageApiKey!;
22
22
  const result = await wrapProvider("alphavantage", () => getOverview(args.symbol.toUpperCase(), apiKey));
@@ -96,7 +96,7 @@ export const compsTool: AgentTool<typeof params> = {
96
96
  description:
97
97
  "Compare 2-6 companies side-by-side on key valuation and financial metrics: P/E, Forward P/E, EPS, Profit Margin, Revenue Growth, Dividend Yield, Beta. Identifies the cheapest and most expensive on each metric.",
98
98
  parameters: params,
99
- async execute(toolCallId, args) {
99
+ async execute(_toolCallId, args) {
100
100
  return withCredentialCheck("alpha_vantage", async () => {
101
101
  const config = getConfig();
102
102
  const symbols = args.symbols.map((s) => s.toUpperCase());
@@ -148,7 +148,7 @@ export const dcfTool: AgentTool<typeof params> = {
148
148
  description:
149
149
  "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.",
150
150
  parameters: params,
151
- async execute(toolCallId, args) {
151
+ async execute(_toolCallId, args) {
152
152
  return withCredentialCheck("alpha_vantage", async () => {
153
153
  const symbol = args.symbol.toUpperCase();
154
154
  const config = getConfig();
@@ -16,7 +16,7 @@ export const earningsTool: AgentTool<typeof params, EarningsData | { credentialR
16
16
  description:
17
17
  "Get quarterly earnings: reported EPS, estimated EPS, and surprise percentage for the last 8 quarters. Requires Alpha Vantage.",
18
18
  parameters: params,
19
- async execute(toolCallId, args) {
19
+ async execute(_toolCallId, args) {
20
20
  return withCredentialCheck("alpha_vantage", async () => {
21
21
  const apiKey = getConfig().alphaVantageApiKey!;
22
22
  const result = await wrapProvider("alphavantage", () => getEarnings(args.symbol.toUpperCase(), apiKey));
@@ -16,7 +16,7 @@ export const financialsTool: AgentTool<typeof params, FinancialStatement[] | { c
16
16
  description:
17
17
  "Get annual income statement data: revenue, gross profit, operating income, net income, and EPS. Requires Alpha Vantage.",
18
18
  parameters: params,
19
- async execute(toolCallId, args) {
19
+ async execute(_toolCallId, args) {
20
20
  return withCredentialCheck("alpha_vantage", async () => {
21
21
  const apiKey = getConfig().alphaVantageApiKey!;
22
22
  const result = await wrapProvider("alphavantage", () =>
@@ -13,6 +13,9 @@ const params = Type.Object({
13
13
  limit: Type.Optional(
14
14
  Type.Number({ description: "Maximum number of filings to return (default: 10)" }),
15
15
  ),
16
+ include_snippets: Type.Optional(
17
+ Type.Boolean({ description: "Include short filing-text evidence snippets for risk/MD&A/litigation themes. Default: true." }),
18
+ ),
16
19
  });
17
20
 
18
21
  export const secFilingsTool: AgentTool<typeof params> = {
@@ -21,12 +24,15 @@ export const secFilingsTool: AgentTool<typeof params> = {
21
24
  description:
22
25
  "Search SEC EDGAR for company filings (10-K annual reports, 10-Q quarterly reports, 8-K material events). Returns filing dates, types, and direct links to the documents. Free API, no key required.",
23
26
  parameters: params,
24
- async execute(toolCallId, args) {
27
+ async execute(_toolCallId, args) {
25
28
  const symbol = args.symbol.toUpperCase();
26
29
  const formTypes = args.form_types ?? ["10-K", "10-Q", "8-K"];
27
30
  const limit = args.limit ?? 10;
31
+ const includeSnippets = args.include_snippets ?? true;
28
32
 
29
- const result = await wrapProvider("sec-edgar", () => searchFilings(symbol, formTypes, limit));
33
+ const result = await wrapProvider("sec-edgar", () =>
34
+ searchFilings(symbol, formTypes, limit, { includeSnippets, snippetLimitPerFiling: 2 })
35
+ );
30
36
  if (result.status === "unavailable") {
31
37
  return {
32
38
  content: [{ type: "text", text: `⚠ SEC filings unavailable for ${symbol} (${result.reason}).` }],
@@ -51,6 +57,23 @@ export const secFilingsTool: AgentTool<typeof params> = {
51
57
  ``,
52
58
  `Links:`,
53
59
  ...filings.map((f) => ` ${f.formType} (${f.filedDate}): ${f.url}`),
60
+ ``,
61
+ `Primary documents:`,
62
+ ...filings.flatMap((f) => [
63
+ ` ${f.formType} (${f.filedDate}): ${f.primaryDocumentUrl ?? "N/A"}`,
64
+ ...(f.items?.length ? [` 8-K items: ${f.items.join(", ")}`] : []),
65
+ ]),
66
+ ...(includeSnippets
67
+ ? [
68
+ ``,
69
+ `Evidence snippets (short excerpts; review source filing for full context):`,
70
+ ...filings.flatMap((f) =>
71
+ (f.evidenceSnippets?.length
72
+ ? f.evidenceSnippets.map((snippet) => ` ${f.formType} ${f.filedDate}: ${snippet}`)
73
+ : [` ${f.formType} ${f.filedDate}: No keyword snippet found in fetched primary document.`])
74
+ ),
75
+ ]
76
+ : []),
54
77
  ];
55
78
 
56
79
  return {
@@ -16,6 +16,7 @@ import { portfolioTrackerTool } from "./portfolio/tracker.js";
16
16
  import { riskAnalysisTool } from "./portfolio/risk-analysis.js";
17
17
  import { watchlistTool } from "./portfolio/watchlist.js";
18
18
  import { correlationTool } from "./portfolio/correlation.js";
19
+ import { holdingsOverlapTool } from "./portfolio/holdings-overlap.js";
19
20
  import { optionChainTool } from "./options/option-chain.js";
20
21
  import { dcfTool } from "./fundamentals/dcf.js";
21
22
  import { compsTool } from "./fundamentals/comps.js";
@@ -52,6 +53,7 @@ export { portfolioTrackerTool } from "./portfolio/tracker.js";
52
53
  export { riskAnalysisTool } from "./portfolio/risk-analysis.js";
53
54
  export { watchlistTool } from "./portfolio/watchlist.js";
54
55
  export { correlationTool } from "./portfolio/correlation.js";
56
+ export { holdingsOverlapTool } from "./portfolio/holdings-overlap.js";
55
57
  export { predictionsTool } from "./portfolio/predictions.js";
56
58
  export { optionChainTool } from "./options/option-chain.js";
57
59
 
@@ -78,6 +80,7 @@ export function getAllTools(): AgentTool<any>[] {
78
80
  riskAnalysisTool,
79
81
  watchlistTool,
80
82
  correlationTool,
83
+ holdingsOverlapTool,
81
84
  predictionsTool,
82
85
  optionChainTool,
83
86
  webSearchTool,
@@ -12,7 +12,7 @@ export const fearGreedTool: AgentTool<typeof params, FearGreedData> = {
12
12
  description:
13
13
  "Get the Crypto Fear & Greed Index (alternative.me) — a sentiment indicator from 0 (Extreme Fear) to 100 (Extreme Greed). Includes current value and previous close.",
14
14
  parameters: params,
15
- async execute(toolCallId, _args) {
15
+ async execute(_toolCallId, _args) {
16
16
  const result = await wrapProvider("feargreed", () => getFearGreedIndex());
17
17
  if (result.status === "unavailable") {
18
18
  return {
@@ -22,14 +22,15 @@ export const fredDataTool: AgentTool<typeof params, FredSeries | { credentialReq
22
22
  description:
23
23
  "Get economic data from FRED (Federal Reserve Economic Data): interest rates, CPI, GDP, unemployment, yield curve, and more. Requires FRED.",
24
24
  parameters: params,
25
- async execute(toolCallId, args) {
25
+ async execute(_toolCallId, args) {
26
26
  return withCredentialCheck("fred", async () => {
27
27
  const apiKey = getConfig().fredApiKey!;
28
- const limit = args.limit ?? 30;
29
- const result = await wrapProvider("fred", () => getSeries(args.series_id.toUpperCase(), apiKey, limit));
28
+ const seriesId = args.series_id.toUpperCase();
29
+ const limit = normalizeLimit(seriesId, args.limit);
30
+ const result = await wrapProvider("fred", () => getSeries(seriesId, apiKey, limit));
30
31
  if (result.status === "unavailable") {
31
32
  return {
32
- content: [{ type: "text", text: `⚠ FRED data unavailable for ${args.series_id.toUpperCase()} (${result.reason}).` }],
33
+ content: [{ type: "text", text: `⚠ FRED data unavailable for ${seriesId} (${result.reason}).` }],
33
34
  details: null as any,
34
35
  };
35
36
  }
@@ -39,12 +40,15 @@ export const fredDataTool: AgentTool<typeof params, FredSeries | { credentialReq
39
40
  const header = `**${series.title}** (${series.id})`;
40
41
  const meta = `Units: ${series.units} | Frequency: ${series.frequency} | Last updated: ${series.lastUpdated}`;
41
42
  const current = latest ? `Latest: ${latest.value} (${latest.date})` : "No data";
43
+ const derived = formatDerivedChange(series);
42
44
 
43
45
  // Show last 10 observations
44
46
  const recent = series.observations.slice(-10);
45
47
  const table = recent.map((o) => `${o.date}: ${o.value}`).join("\n");
46
48
 
47
- const text = [header, meta, current, "", "Recent observations:", table].join("\n");
49
+ const text = [header, meta, current, derived, "", "Recent observations:", table]
50
+ .filter(Boolean)
51
+ .join("\n");
48
52
  const prefix = result.stale
49
53
  ? `⚠ Using cached FRED data from ${result.timestamp} (FRED unavailable)\n`
50
54
  : "";
@@ -52,3 +56,25 @@ export const fredDataTool: AgentTool<typeof params, FredSeries | { credentialReq
52
56
  });
53
57
  },
54
58
  };
59
+
60
+ function normalizeLimit(seriesId: string, requested: number | undefined): number {
61
+ const limit = requested ?? 30;
62
+ if (seriesId === FRED_SERIES.CPI && limit < 13) return 13;
63
+ return limit;
64
+ }
65
+
66
+ function formatDerivedChange(series: FredSeries): string | null {
67
+ const latest = series.observations.at(-1);
68
+ if (!latest) return null;
69
+ if (!/monthly/i.test(series.frequency) || !/\bindex\b/i.test(series.units)) return null;
70
+
71
+ const latestYear = Number(latest.date.slice(0, 4));
72
+ const monthDay = latest.date.slice(4);
73
+ const prior = series.observations.find((observation) =>
74
+ observation.date === `${latestYear - 1}${monthDay}`
75
+ );
76
+ if (!prior || prior.value === 0) return null;
77
+
78
+ const pct = ((latest.value / prior.value) - 1) * 100;
79
+ return `Derived YoY change: ${pct.toFixed(2)}% (${prior.date} to ${latest.date}).`;
80
+ }
@@ -3,6 +3,7 @@ import type { AgentTool } from "@earendil-works/pi-agent-core";
3
3
  import { getCryptoHistory } from "../../providers/coingecko.js";
4
4
  import { wrapProvider } from "../../providers/wrap-provider.js";
5
5
  import type { OHLCV } from "../../types/market.js";
6
+ import { computeRiskMetrics } from "../portfolio/risk-analysis.js";
6
7
 
7
8
  const params = Type.Object({
8
9
  id: Type.String({
@@ -20,7 +21,7 @@ export const cryptoHistoryTool: AgentTool<typeof params, OHLCV[]> = {
20
21
  label: "Crypto History",
21
22
  description: "Get historical OHLC data for a cryptocurrency",
22
23
  parameters: params,
23
- async execute(toolCallId, args) {
24
+ async execute(_toolCallId, args) {
24
25
  const id = args.id.toLowerCase();
25
26
  const days = args.days ?? 180;
26
27
  const result = await wrapProvider("coingecko", () => getCryptoHistory(id, days));
@@ -45,7 +46,22 @@ export const cryptoHistoryTool: AgentTool<typeof params, OHLCV[]> = {
45
46
  )
46
47
  .join("\n");
47
48
 
48
- const text = [...summary, "", "Recent bars:", table].join("\n");
49
+ const riskLines = buildRiskLines(id, bars);
50
+ const text = [...summary, ...riskLines, "", "Recent bars:", table].join("\n");
49
51
  return { content: [{ type: "text", text }], details: bars };
50
52
  },
51
53
  };
54
+
55
+ function buildRiskLines(id: string, bars: OHLCV[]): string[] {
56
+ if (bars.length < 30) return [];
57
+ const metrics = computeRiskMetrics(id.toUpperCase(), bars.map((bar) => bar.close));
58
+ return [
59
+ "",
60
+ "Risk metrics from crypto history:",
61
+ `Annualized Return: ${(metrics.annualizedReturn * 100).toFixed(2)}%`,
62
+ `Annualized Volatility: ${(metrics.annualizedVolatility * 100).toFixed(2)}%`,
63
+ `Sharpe Ratio: ${metrics.sharpeRatio.toFixed(2)}`,
64
+ `Max Drawdown: ${(metrics.maxDrawdown * 100).toFixed(2)}%`,
65
+ `Value at Risk (95%, daily): ${(metrics.var95 * 100).toFixed(2)}%`,
66
+ ];
67
+ }
@@ -17,7 +17,7 @@ export const cryptoPriceTool: AgentTool<typeof params, CryptoPrice> = {
17
17
  description:
18
18
  "Get current crypto price, 24h change, market cap, volume, ATH, and supply data",
19
19
  parameters: params,
20
- async execute(toolCallId, args) {
20
+ async execute(_toolCallId, args) {
21
21
  const result = await wrapProvider("coingecko", () => getCryptoPrice(args.id.toLowerCase()));
22
22
  if (result.status === "unavailable") {
23
23
  return {
@@ -25,7 +25,7 @@ export const searchTickerTool: AgentTool<typeof params> = {
25
25
  description:
26
26
  "Search for any ticker symbol — stocks, crypto, ETFs, indices, forex. Returns matching symbols with names and exchange info. Use this when you don't know the exact ticker for an asset.",
27
27
  parameters: params,
28
- async execute(toolCallId, args) {
28
+ async execute(_toolCallId, args) {
29
29
  const url = `https://query1.finance.yahoo.com/v1/finance/search?q=${encodeURIComponent(args.query)}&quotesCount=10&newsCount=0`;
30
30
  const data = await httpGet<YahooSearchResponse>(url, {
31
31
  headers: { "User-Agent": "OpenCandle/1.0" },
@@ -29,7 +29,7 @@ export const stockHistoryTool: AgentTool<typeof params, OHLCV[]> = {
29
29
  label: "Stock History",
30
30
  description: "Get historical OHLCV (open, high, low, close, volume) data for a stock",
31
31
  parameters: params,
32
- async execute(toolCallId, args) {
32
+ async execute(_toolCallId, args) {
33
33
  const symbol = args.symbol.toUpperCase();
34
34
  const range = args.range ?? "6mo";
35
35
  const interval = args.interval ?? "1d";
@@ -16,7 +16,7 @@ export const stockQuoteTool: AgentTool<typeof params, StockQuote> = {
16
16
  description:
17
17
  "Get real-time stock price, volume, market cap, and 52-week range for a ticker symbol",
18
18
  parameters: params,
19
- async execute(toolCallId, args) {
19
+ async execute(_toolCallId, args) {
20
20
  const symbol = args.symbol.toUpperCase();
21
21
  const apiKey = getConfig().alphaVantageApiKey;
22
22
 
@@ -47,7 +47,6 @@ export function computeGreeks(input: GreeksInput): Greeks {
47
47
  rho: (strike * timeYears * expRT * nd2) / 100, // per 1% change in rate
48
48
  };
49
49
  } else {
50
- const nMinusD1 = cdf(-d1);
51
50
  const nMinusD2 = cdf(-d2);
52
51
  return {
53
52
  delta: nd1 - 1,
@@ -25,7 +25,7 @@ export const optionChainTool: AgentTool<typeof params, OptionsChain> = {
25
25
  description:
26
26
  "Get the full options chain for a stock with strikes, bids, asks, volume, open interest, implied volatility, and computed Greeks (Delta, Gamma, Theta, Vega, Rho via Black-Scholes). No API key required.",
27
27
  parameters: params,
28
- async execute(toolCallId, args) {
28
+ async execute(_toolCallId, args) {
29
29
  const symbol = args.symbol.toUpperCase();
30
30
  const normalizedType = args.type?.toLowerCase();
31
31
  const expirationTs = args.expiration
@@ -44,6 +44,11 @@ export const optionChainTool: AgentTool<typeof params, OptionsChain> = {
44
44
  const lines: string[] = [
45
45
  `**${chain.symbol} Options Chain** — Expiry: ${chain.expirationDate}`,
46
46
  `Underlying: $${chain.underlyingPrice.toFixed(2)}`,
47
+ `Quote status: ${chain.quoteStatus.marketSession} / ${chain.quoteStatus.bidAskState}`,
48
+ "Option bid/ask and last prices are quoted per share; multiply by 100 for one standard contract premium.",
49
+ ...(chain.quoteStatus.warning
50
+ ? [`⚠ ${chain.quoteStatus.warning} do not treat zero bid/ask as confirmed live illiquidity without broker verification; do not stop at the stale quote caveat. Disclose the gap, avoid naming tradable live premiums, and finish the strategy explanation with mechanics, assignment outcomes, labeled hypotheticals, and what live broker quotes would change.`]
51
+ : []),
47
52
  `Available expirations: ${formatAvailableExpirations(chain.expirationDates)}`,
48
53
  "",
49
54
  ];
@@ -53,7 +58,7 @@ export const optionChainTool: AgentTool<typeof params, OptionsChain> = {
53
58
 
54
59
  if (showCalls && chain.calls.length > 0) {
55
60
  lines.push(`**CALLS** (${chain.calls.length} contracts, volume: ${chain.totalCallVolume.toLocaleString()})`);
56
- lines.push("Strike | Bid/Ask | Last | Vol | OI | IV | Delta | Theta");
61
+ lines.push("Strike | Bid/Ask (per share) | Last (per share) | Vol | OI | IV | Delta | Gamma | Theta | Vega | Rho");
57
62
  const topCalls = sortByVolume(chain.calls).slice(0, 10);
58
63
  for (const c of topCalls) {
59
64
  lines.push(formatContract(c));
@@ -63,7 +68,7 @@ export const optionChainTool: AgentTool<typeof params, OptionsChain> = {
63
68
 
64
69
  if (showPuts && chain.puts.length > 0) {
65
70
  lines.push(`**PUTS** (${chain.puts.length} contracts, volume: ${chain.totalPutVolume.toLocaleString()})`);
66
- lines.push("Strike | Bid/Ask | Last | Vol | OI | IV | Delta | Theta");
71
+ lines.push("Strike | Bid/Ask (per share) | Last (per share) | Vol | OI | IV | Delta | Gamma | Theta | Vega | Rho");
67
72
  const topPuts = sortByVolume(chain.puts).slice(0, 10);
68
73
  for (const c of topPuts) {
69
74
  lines.push(formatContract(c));
@@ -87,5 +92,5 @@ function formatAvailableExpirations(expirationDates: string[]): string {
87
92
 
88
93
  function formatContract(c: OptionContract): string {
89
94
  const itm = c.inTheMoney ? "*" : " ";
90
- return `${itm}$${c.strike.toFixed(2)} | $${c.bid.toFixed(2)}/$${c.ask.toFixed(2)} | $${c.lastPrice.toFixed(2)} | ${c.volume} | ${c.openInterest} | ${(c.impliedVolatility * 100).toFixed(1)}% | ${c.greeks.delta.toFixed(3)} | ${c.greeks.theta.toFixed(3)}`;
95
+ return `${itm}$${c.strike.toFixed(2)} | $${c.bid.toFixed(2)}/$${c.ask.toFixed(2)} | $${c.lastPrice.toFixed(2)} | ${c.volume} | ${c.openInterest} | ${(c.impliedVolatility * 100).toFixed(1)}% | ${c.greeks.delta.toFixed(3)} | ${c.greeks.gamma.toFixed(3)} | ${c.greeks.theta.toFixed(3)} | ${c.greeks.vega.toFixed(3)} | ${c.greeks.rho.toFixed(3)}`;
91
96
  }
@@ -86,7 +86,7 @@ export const correlationTool: AgentTool<typeof params> = {
86
86
  description:
87
87
  "Compute pairwise return correlations between 2+ stocks. Identifies highly correlated positions (|r| > 0.7) as concentration risk. Useful for portfolio diversification analysis.",
88
88
  parameters: params,
89
- async execute(toolCallId, args) {
89
+ async execute(_toolCallId, args) {
90
90
  const symbols = args.symbols.map((s) => s.toUpperCase());
91
91
  const period = args.period ?? "1y";
92
92