opencandle 0.3.0 → 0.4.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.
- package/assets/logo.svg +187 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +38 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +10 -0
- package/dist/infra/browser.js +1 -0
- package/dist/infra/browser.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +1 -0
- package/dist/infra/native-dependencies.js +10 -0
- package/dist/infra/native-dependencies.js.map +1 -0
- package/dist/infra/node-version.d.ts +2 -0
- package/dist/infra/node-version.js +23 -0
- package/dist/infra/node-version.js.map +1 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/sqlite.js +42 -4
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.d.ts +6 -0
- package/dist/memory/storage.js +3 -3
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.d.ts +8 -0
- package/dist/memory/tool-defaults.js +59 -0
- package/dist/memory/tool-defaults.js.map +1 -0
- package/dist/onboarding/connect.d.ts +13 -1
- package/dist/onboarding/connect.js +21 -10
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/prompt-user.d.ts +1 -1
- package/dist/onboarding/providers.d.ts +7 -0
- package/dist/onboarding/providers.js +6 -3
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +1 -1
- package/dist/pi/opencandle-extension.d.ts +7 -1
- package/dist/pi/opencandle-extension.js +186 -10
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-storage.d.ts +2 -0
- package/dist/pi/session-storage.js +5 -0
- package/dist/pi/session-storage.js.map +1 -0
- package/dist/pi/session.d.ts +4 -1
- package/dist/pi/session.js +25 -3
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.d.ts +1 -1
- package/dist/pi/setup.js +1 -1
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +2 -2
- package/dist/pi/tool-adapter.js +14 -1
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +22 -0
- package/dist/prompts/context-builder.js +45 -10
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/disclaimer.d.ts +6 -0
- package/dist/prompts/disclaimer.js +9 -0
- package/dist/prompts/disclaimer.js.map +1 -0
- package/dist/prompts/workflow-prompts.d.ts +8 -0
- package/dist/prompts/workflow-prompts.js +39 -5
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/yahoo-finance.js +70 -33
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/defaults.js +1 -1
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +3 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/router-llm-client.d.ts +11 -0
- package/dist/routing/router-llm-client.js +42 -0
- package/dist/routing/router-llm-client.js.map +1 -0
- package/dist/routing/router-prompt.d.ts +2 -0
- package/dist/routing/router-prompt.js +138 -0
- package/dist/routing/router-prompt.js.map +1 -0
- package/dist/routing/router-types.d.ts +62 -0
- package/dist/routing/router-types.js +2 -0
- package/dist/routing/router-types.js.map +1 -0
- package/dist/routing/router.d.ts +10 -0
- package/dist/routing/router.js +194 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/runtime/session-coordinator.d.ts +63 -3
- package/dist/runtime/session-coordinator.js +155 -4
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
- package/dist/runtime/tool-defaults-wrapper.js +25 -0
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
- package/dist/sentiment/store.js +5 -0
- package/dist/sentiment/store.js.map +1 -1
- package/dist/system-prompt.js +20 -12
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +4 -4
- package/dist/tools/fundamentals/company-overview.d.ts +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +1 -1
- package/dist/tools/fundamentals/financials.d.ts +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
- package/dist/tools/index.d.ts +28 -1
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.d.ts +1 -1
- package/dist/tools/interaction/twitter-login.d.ts +1 -1
- package/dist/tools/macro/fear-greed.d.ts +1 -1
- package/dist/tools/macro/fred-data.d.ts +1 -1
- package/dist/tools/market/crypto-history.d.ts +1 -1
- package/dist/tools/market/crypto-price.d.ts +1 -1
- package/dist/tools/market/search-ticker.d.ts +1 -1
- package/dist/tools/market/stock-history.d.ts +1 -1
- package/dist/tools/market/stock-quote.d.ts +1 -1
- package/dist/tools/options/option-chain.d.ts +1 -1
- package/dist/tools/options/option-chain.js +4 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/predictions.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/tracker.d.ts +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/web-search.d.ts +1 -1
- package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
- package/dist/tools/technical/backtest.d.ts +1 -1
- package/dist/tools/technical/indicators.d.ts +1 -1
- package/dist/tools/technical/indicators.js +7 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/workflows/options-screener.js +7 -2
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +3 -3
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/background-quotes.ts +31 -0
- package/gui/server/chat-event-adapter.ts +142 -0
- package/gui/server/invoke-tool.ts +89 -0
- package/gui/server/live-chat-event-adapter.ts +181 -0
- package/gui/server/model-setup.ts +100 -0
- package/gui/server/package.json +5 -0
- package/gui/server/projector.ts +212 -0
- package/gui/server/server.ts +592 -0
- package/gui/server/session-actions.ts +31 -0
- package/gui/server/tool-metadata.ts +88 -0
- package/gui/server/websocket.ts +128 -0
- package/gui/server/writer-lock.ts +118 -0
- package/gui/shared/chat-events.ts +118 -0
- package/gui/shared/event-reducer.ts +186 -0
- package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
- package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
- package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
- package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
- package/gui/web/dist/index.html +17 -0
- package/package.json +44 -18
- package/src/analysts/contracts.ts +189 -0
- package/src/analysts/orchestrator.ts +300 -0
- package/src/cli.ts +205 -0
- package/src/config.ts +161 -0
- package/src/index.ts +5 -0
- package/src/infra/browser.ts +111 -0
- package/src/infra/cache.ts +103 -0
- package/src/infra/http-client.ts +68 -0
- package/src/infra/index.ts +18 -0
- package/src/infra/native-dependencies.ts +12 -0
- package/src/infra/node-version.ts +24 -0
- package/src/infra/open-url.ts +28 -0
- package/src/infra/opencandle-paths.ts +64 -0
- package/src/infra/rate-limiter.ts +64 -0
- package/src/memory/index.ts +10 -0
- package/src/memory/manager.ts +159 -0
- package/src/memory/preference-extractor.ts +106 -0
- package/src/memory/retrieval.ts +70 -0
- package/src/memory/sqlite.ts +172 -0
- package/src/memory/storage.ts +204 -0
- package/src/memory/tool-defaults.ts +87 -0
- package/src/memory/types.ts +67 -0
- package/src/onboarding/connect.ts +184 -0
- package/src/onboarding/credential-interceptor.ts +134 -0
- package/src/onboarding/degradation-accumulator.ts +79 -0
- package/src/onboarding/prompt-user.ts +85 -0
- package/src/onboarding/providers.ts +315 -0
- package/src/onboarding/state.ts +218 -0
- package/src/onboarding/tool-helpers.ts +111 -0
- package/src/onboarding/tool-tags.ts +201 -0
- package/src/onboarding/validation.ts +158 -0
- package/src/pi/opencandle-extension.ts +724 -0
- package/src/pi/session-storage.ts +5 -0
- package/src/pi/session.ts +81 -0
- package/src/pi/setup.ts +371 -0
- package/src/pi/tool-adapter.ts +36 -0
- package/src/prompts/context-builder.ts +204 -0
- package/src/prompts/disclaimer.ts +9 -0
- package/src/prompts/sections.ts +46 -0
- package/src/prompts/workflow-prompts.ts +279 -0
- package/src/providers/alpha-vantage.ts +292 -0
- package/src/providers/coingecko.ts +96 -0
- package/src/providers/exa-search.ts +373 -0
- package/src/providers/fear-greed.ts +45 -0
- package/src/providers/finnhub.ts +124 -0
- package/src/providers/fred.ts +83 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider-credential-error.ts +23 -0
- package/src/providers/reddit.ts +151 -0
- package/src/providers/sec-edgar.ts +96 -0
- package/src/providers/twitter.ts +173 -0
- package/src/providers/web-search.ts +293 -0
- package/src/providers/with-fallback.ts +41 -0
- package/src/providers/wrap-provider.ts +64 -0
- package/src/providers/yahoo-finance.ts +367 -0
- package/src/routing/classify-intent.ts +194 -0
- package/src/routing/defaults.ts +29 -0
- package/src/routing/entity-extractor.ts +140 -0
- package/src/routing/index.ts +26 -0
- package/src/routing/router-llm-client.ts +51 -0
- package/src/routing/router-prompt.ts +159 -0
- package/src/routing/router-types.ts +66 -0
- package/src/routing/router.ts +213 -0
- package/src/routing/slot-resolver.ts +152 -0
- package/src/routing/types.ts +63 -0
- package/src/runtime/evidence.ts +77 -0
- package/src/runtime/index.ts +55 -0
- package/src/runtime/prompt-step.ts +75 -0
- package/src/runtime/provider-ids.ts +15 -0
- package/src/runtime/provider-tracker.ts +40 -0
- package/src/runtime/run-context.ts +22 -0
- package/src/runtime/session-coordinator.ts +406 -0
- package/src/runtime/tool-defaults-wrapper.ts +35 -0
- package/src/runtime/validation.ts +214 -0
- package/src/runtime/workflow-events.ts +75 -0
- package/src/runtime/workflow-runner.ts +188 -0
- package/src/runtime/workflow-types.ts +102 -0
- package/src/sentiment/adapters/finnhub.ts +44 -0
- package/src/sentiment/adapters/reddit.ts +65 -0
- package/src/sentiment/adapters/twitter.ts +36 -0
- package/src/sentiment/adapters/web.ts +44 -0
- package/src/sentiment/index.ts +58 -0
- package/src/sentiment/keywords.ts +9 -0
- package/src/sentiment/pipeline.ts +68 -0
- package/src/sentiment/scorer.ts +78 -0
- package/src/sentiment/store.ts +260 -0
- package/src/sentiment/trends.ts +90 -0
- package/src/sentiment/types.ts +108 -0
- package/src/system-prompt.ts +115 -0
- package/src/tool-kit.ts +68 -0
- package/src/tools/AGENTS.md +36 -0
- package/src/tools/fundamentals/company-overview.ts +54 -0
- package/src/tools/fundamentals/comps.ts +156 -0
- package/src/tools/fundamentals/dcf.ts +267 -0
- package/src/tools/fundamentals/earnings.ts +47 -0
- package/src/tools/fundamentals/financials.ts +54 -0
- package/src/tools/fundamentals/sec-filings.ts +61 -0
- package/src/tools/index.ts +88 -0
- package/src/tools/interaction/ask-user.ts +81 -0
- package/src/tools/interaction/twitter-login.ts +93 -0
- package/src/tools/macro/fear-greed.ts +41 -0
- package/src/tools/macro/fred-data.ts +54 -0
- package/src/tools/market/crypto-history.ts +51 -0
- package/src/tools/market/crypto-price.ts +53 -0
- package/src/tools/market/search-ticker.ts +53 -0
- package/src/tools/market/stock-history.ts +79 -0
- package/src/tools/market/stock-quote.ts +64 -0
- package/src/tools/options/greeks.ts +82 -0
- package/src/tools/options/option-chain.ts +91 -0
- package/src/tools/portfolio/correlation.ts +162 -0
- package/src/tools/portfolio/predictions.ts +253 -0
- package/src/tools/portfolio/risk-analysis.ts +134 -0
- package/src/tools/portfolio/tracker.ts +147 -0
- package/src/tools/portfolio/watchlist.ts +153 -0
- package/src/tools/sentiment/reddit-sentiment.ts +164 -0
- package/src/tools/sentiment/sentiment-summary.ts +256 -0
- package/src/tools/sentiment/sentiment-trend.ts +58 -0
- package/src/tools/sentiment/twitter-sentiment.ts +96 -0
- package/src/tools/sentiment/web-search.ts +150 -0
- package/src/tools/sentiment/web-sentiment.ts +76 -0
- package/src/tools/technical/backtest.ts +246 -0
- package/src/tools/technical/indicators.ts +258 -0
- package/src/types/fundamentals.ts +46 -0
- package/src/types/index.ts +20 -0
- package/src/types/macro.ts +27 -0
- package/src/types/market.ts +43 -0
- package/src/types/options.ts +35 -0
- package/src/types/portfolio.ts +41 -0
- package/src/types/sentiment.ts +70 -0
- package/src/workflows/compare-assets.ts +39 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/options-screener.ts +49 -0
- package/src/workflows/portfolio-builder.ts +52 -0
- package/src/workflows/types.ts +4 -0
package/src/tool-kit.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { agentToolToPiTool } from "./pi/tool-adapter.js";
|
|
5
|
+
|
|
6
|
+
// Re-exports for tool authors — import from "opencandle/tool-kit"
|
|
7
|
+
export { cache, Cache, TTL } from "./infra/cache.js";
|
|
8
|
+
export { rateLimiter, RateLimiter } from "./infra/rate-limiter.js";
|
|
9
|
+
export { httpGet, HttpError, type HttpClientOptions } from "./infra/http-client.js";
|
|
10
|
+
export { agentToolToPiTool } from "./pi/tool-adapter.js";
|
|
11
|
+
export { Type } from "@sinclair/typebox";
|
|
12
|
+
export type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
export type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
14
|
+
|
|
15
|
+
// Module-level registry — all extensions run in the same Node.js process,
|
|
16
|
+
// so keep a deduped index keyed by tool name.
|
|
17
|
+
const addonToolRegistry = new Map<string, { name: string; description: string }>();
|
|
18
|
+
|
|
19
|
+
export function getAddonToolDescriptions(): ReadonlyArray<{ name: string; description: string }> {
|
|
20
|
+
return Array.from(addonToolRegistry.values());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SNAKE_CASE_VERB_RE = /^(get|analyze|search|calculate|compare|compute|track|manage|backtest|list|fetch|check)_[a-z][a-z0-9_]*$/;
|
|
24
|
+
|
|
25
|
+
export interface ToolConfig<TParams extends TSchema, TDetails = unknown> {
|
|
26
|
+
name: string;
|
|
27
|
+
label: string;
|
|
28
|
+
description: string;
|
|
29
|
+
parameters: TParams;
|
|
30
|
+
execute: AgentTool<TParams, TDetails>["execute"];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createTool<TParams extends TSchema, TDetails = unknown>(
|
|
34
|
+
config: ToolConfig<TParams, TDetails>,
|
|
35
|
+
): AgentTool<TParams, TDetails> {
|
|
36
|
+
if (!config.name || !SNAKE_CASE_VERB_RE.test(config.name)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Invalid tool name "${config.name}": must be snake_case and start with a verb prefix ` +
|
|
39
|
+
`(get_, analyze_, search_, calculate_, compare_, compute_, track_, manage_, backtest_, list_, fetch_, check_)`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (!config.description || config.description.trim().length === 0) {
|
|
43
|
+
throw new Error(`Tool "${config.name}" requires a non-empty description`);
|
|
44
|
+
}
|
|
45
|
+
if (!config.parameters) {
|
|
46
|
+
throw new Error(`Tool "${config.name}" requires parameters (Typebox schema)`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
name: config.name,
|
|
50
|
+
label: config.label,
|
|
51
|
+
description: config.description,
|
|
52
|
+
parameters: config.parameters,
|
|
53
|
+
execute: config.execute,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerTools<TParams extends TSchema>(
|
|
58
|
+
pi: ExtensionAPI,
|
|
59
|
+
tools: AgentTool<TParams>[],
|
|
60
|
+
): void {
|
|
61
|
+
for (const tool of tools) {
|
|
62
|
+
if (addonToolRegistry.has(tool.name)) {
|
|
63
|
+
console.warn(`[opencandle] Warning: tool "${tool.name}" already registered (overwriting)`);
|
|
64
|
+
}
|
|
65
|
+
pi.registerTool(agentToolToPiTool(tool));
|
|
66
|
+
addonToolRegistry.set(tool.name, { name: tool.name, description: tool.description });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# TOOLS
|
|
2
|
+
|
|
3
|
+
Market analysis tools organized by financial domain. Tools fetch data from `src/providers/`, return structured results for LLM synthesis.
|
|
4
|
+
|
|
5
|
+
## STRUCTURE
|
|
6
|
+
```
|
|
7
|
+
src/tools/
|
|
8
|
+
├── fundamentals/ # Earnings, financials, DCF, comps, SEC filings
|
|
9
|
+
├── technical/ # Indicators (SMA, RSI, MACD), backtesting
|
|
10
|
+
├── options/ # Options chains, Greeks computation
|
|
11
|
+
├── macro/ # FRED economic data, fear & greed index
|
|
12
|
+
├── sentiment/ # Reddit sentiment, news sentiment
|
|
13
|
+
├── portfolio/ # Tracker, risk analysis, watchlist, correlation, predictions
|
|
14
|
+
├── market/ # Stock quotes, history, crypto, ticker search
|
|
15
|
+
└── index.ts # getAllTools() registry — add new tools here
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## LOOKUP
|
|
19
|
+
| Domain | Location |
|
|
20
|
+
|--------|----------|
|
|
21
|
+
| P/E, EPS, balance sheets, DCF | `fundamentals/` |
|
|
22
|
+
| RSI, MACD, SMA, backtest | `technical/` |
|
|
23
|
+
| Put/call ratio, IV, Greeks | `options/` |
|
|
24
|
+
| GDP, inflation, Fed rates | `macro/` |
|
|
25
|
+
| Reddit buzz, news | `sentiment/` |
|
|
26
|
+
| Positions, Sharpe, VaR, watchlist | `portfolio/` |
|
|
27
|
+
| Quotes, OHLCV, crypto, search | `market/` |
|
|
28
|
+
|
|
29
|
+
## ADDING A TOOL
|
|
30
|
+
1. Create `src/tools/<domain>/my-tool.ts` with Typebox params + `AgentTool` export.
|
|
31
|
+
2. Register in `src/tools/index.ts` (`getAllTools()` array).
|
|
32
|
+
3. Add test in `tests/unit/tools/` with fixture-based fetch mocking.
|
|
33
|
+
|
|
34
|
+
## ANTI-PATTERNS
|
|
35
|
+
- Never hardcode mock data in tools; call providers.
|
|
36
|
+
- Tools return structured data. The LLM analyzes it, not the tool.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getOverview } from "../../providers/alpha-vantage.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import { getConfig } from "../../config.js";
|
|
6
|
+
import { withCredentialCheck } from "../../onboarding/tool-helpers.js";
|
|
7
|
+
import type { CompanyOverview } from "../../types/fundamentals.js";
|
|
8
|
+
|
|
9
|
+
const params = Type.Object({
|
|
10
|
+
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT)" }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const companyOverviewTool: AgentTool<typeof params, CompanyOverview | { credentialRequired: unknown }> = {
|
|
14
|
+
name: "get_company_overview",
|
|
15
|
+
label: "Company Overview",
|
|
16
|
+
description:
|
|
17
|
+
"Get company fundamentals: P/E ratio, EPS, market cap, sector, dividend yield, profit margin, beta, and description. Requires Alpha Vantage.",
|
|
18
|
+
parameters: params,
|
|
19
|
+
async execute(toolCallId, args) {
|
|
20
|
+
return withCredentialCheck("alpha_vantage", async () => {
|
|
21
|
+
const apiKey = getConfig().alphaVantageApiKey!;
|
|
22
|
+
const result = await wrapProvider("alphavantage", () => getOverview(args.symbol.toUpperCase(), apiKey));
|
|
23
|
+
if (result.status === "unavailable") {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: `⚠ Company overview unavailable for ${args.symbol.toUpperCase()} (${result.reason}). Analysis will proceed without fundamentals.` }],
|
|
26
|
+
details: null as any,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const ov = result.data;
|
|
30
|
+
const text = [
|
|
31
|
+
`**${ov.name}** (${ov.symbol}) — ${ov.exchange}`,
|
|
32
|
+
`Sector: ${ov.sector} | Industry: ${ov.industry}`,
|
|
33
|
+
`Market Cap: $${formatLargeNumber(ov.marketCap)} | P/E: ${ov.pe ?? "N/A"} | Fwd P/E: ${ov.forwardPe ?? "N/A"}`,
|
|
34
|
+
`EPS: $${ov.eps ?? "N/A"} | Div Yield: ${ov.dividendYield ? (ov.dividendYield * 100).toFixed(2) + "%" : "N/A"}`,
|
|
35
|
+
`Beta: ${ov.beta ?? "N/A"} | Profit Margin: ${ov.profitMargin ? (ov.profitMargin * 100).toFixed(1) + "%" : "N/A"}`,
|
|
36
|
+
`52W: $${ov.week52Low.toFixed(2)} - $${ov.week52High.toFixed(2)}`,
|
|
37
|
+
``,
|
|
38
|
+
ov.description.slice(0, 300) + (ov.description.length > 300 ? "..." : ""),
|
|
39
|
+
].join("\n");
|
|
40
|
+
|
|
41
|
+
const prefix = result.stale
|
|
42
|
+
? `⚠ Using cached fundamentals from ${result.timestamp} (Alpha Vantage rate limited)\n`
|
|
43
|
+
: "";
|
|
44
|
+
return { content: [{ type: "text", text: prefix + text }], details: ov };
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function formatLargeNumber(n: number): string {
|
|
50
|
+
if (n >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
|
|
51
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
52
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M`;
|
|
53
|
+
return n.toLocaleString();
|
|
54
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getOverview } from "../../providers/alpha-vantage.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import { getConfig } from "../../config.js";
|
|
6
|
+
import { withCredentialCheck } from "../../onboarding/tool-helpers.js";
|
|
7
|
+
import type { CompanyOverview } from "../../types/fundamentals.js";
|
|
8
|
+
|
|
9
|
+
export interface CompsMetric {
|
|
10
|
+
metric: string;
|
|
11
|
+
values: Record<string, number | null>;
|
|
12
|
+
median: number | null;
|
|
13
|
+
p25: number | null;
|
|
14
|
+
p75: number | null;
|
|
15
|
+
best: string;
|
|
16
|
+
worst: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CompsResult {
|
|
20
|
+
companies: CompanyOverview[];
|
|
21
|
+
metrics: CompsMetric[];
|
|
22
|
+
unavailableSymbols: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type MetricDef = {
|
|
26
|
+
name: string;
|
|
27
|
+
extract: (c: CompanyOverview) => number | null;
|
|
28
|
+
lowerIsBetter: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const METRIC_DEFS: MetricDef[] = [
|
|
32
|
+
{ name: "P/E", extract: (c) => c.pe, lowerIsBetter: true },
|
|
33
|
+
{ name: "Forward P/E", extract: (c) => c.forwardPe, lowerIsBetter: true },
|
|
34
|
+
{ name: "EPS", extract: (c) => c.eps, lowerIsBetter: false },
|
|
35
|
+
{ name: "Profit Margin", extract: (c) => c.profitMargin, lowerIsBetter: false },
|
|
36
|
+
{ name: "Revenue Growth", extract: (c) => c.revenueGrowth, lowerIsBetter: false },
|
|
37
|
+
{ name: "Dividend Yield", extract: (c) => c.dividendYield, lowerIsBetter: false },
|
|
38
|
+
{ name: "Beta", extract: (c) => c.beta, lowerIsBetter: true },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export function computeComps(companies: CompanyOverview[]): CompsResult {
|
|
42
|
+
const metrics: CompsMetric[] = METRIC_DEFS.map((def) => {
|
|
43
|
+
const values: Record<string, number | null> = {};
|
|
44
|
+
for (const c of companies) {
|
|
45
|
+
values[c.symbol] = def.extract(c);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const nonNull = Object.entries(values)
|
|
49
|
+
.filter(([, v]) => v != null)
|
|
50
|
+
.map(([sym, v]) => ({ sym, v: v! }));
|
|
51
|
+
|
|
52
|
+
const sorted = [...nonNull].sort((a, b) => a.v - b.v);
|
|
53
|
+
const sortedVals = sorted.map((s) => s.v);
|
|
54
|
+
const median = computeMedian(sortedVals);
|
|
55
|
+
const p25 = computePercentile(sortedVals, 0.25);
|
|
56
|
+
const p75 = computePercentile(sortedVals, 0.75);
|
|
57
|
+
|
|
58
|
+
const best = def.lowerIsBetter ? sorted[0]?.sym ?? "" : sorted[sorted.length - 1]?.sym ?? "";
|
|
59
|
+
const worst = def.lowerIsBetter ? sorted[sorted.length - 1]?.sym ?? "" : sorted[0]?.sym ?? "";
|
|
60
|
+
|
|
61
|
+
return { metric: def.name, values, median, p25, p75, best, worst };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return { companies, metrics, unavailableSymbols: [] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function computeMedian(sorted: number[]): number | null {
|
|
68
|
+
if (sorted.length === 0) return null;
|
|
69
|
+
const mid = Math.floor(sorted.length / 2);
|
|
70
|
+
return sorted.length % 2 === 0
|
|
71
|
+
? (sorted[mid - 1] + sorted[mid]) / 2
|
|
72
|
+
: sorted[mid];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function computePercentile(sorted: number[], p: number): number | null {
|
|
76
|
+
if (sorted.length === 0) return null;
|
|
77
|
+
if (sorted.length === 1) return sorted[0];
|
|
78
|
+
const idx = p * (sorted.length - 1);
|
|
79
|
+
const lower = Math.floor(idx);
|
|
80
|
+
const upper = Math.ceil(idx);
|
|
81
|
+
if (lower === upper) return sorted[lower];
|
|
82
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (idx - lower);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const params = Type.Object({
|
|
86
|
+
symbols: Type.Array(Type.String(), {
|
|
87
|
+
description: "Array of 2-6 ticker symbols to compare (e.g. ['AAPL','MSFT','GOOGL'])",
|
|
88
|
+
minItems: 2,
|
|
89
|
+
maxItems: 6,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const compsTool: AgentTool<typeof params> = {
|
|
94
|
+
name: "compare_companies",
|
|
95
|
+
label: "Comparable Company Analysis",
|
|
96
|
+
description:
|
|
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
|
+
parameters: params,
|
|
99
|
+
async execute(toolCallId, args) {
|
|
100
|
+
return withCredentialCheck("alpha_vantage", async () => {
|
|
101
|
+
const config = getConfig();
|
|
102
|
+
const symbols = args.symbols.map((s) => s.toUpperCase());
|
|
103
|
+
|
|
104
|
+
const results = await Promise.all(
|
|
105
|
+
symbols.map(async (s) => ({
|
|
106
|
+
symbol: s,
|
|
107
|
+
result: await wrapProvider("alphavantage", () => getOverview(s, config.alphaVantageApiKey!)),
|
|
108
|
+
})),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const companies: CompanyOverview[] = [];
|
|
112
|
+
const unavailableSymbols: string[] = [];
|
|
113
|
+
|
|
114
|
+
for (const { symbol: sym, result: r } of results) {
|
|
115
|
+
if (r.status === "ok") {
|
|
116
|
+
companies.push(r.data);
|
|
117
|
+
} else {
|
|
118
|
+
unavailableSymbols.push(sym);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (companies.length === 0) {
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: "text", text: `⚠ Company fundamentals unavailable for all symbols: ${symbols.join(", ")}. Alpha Vantage may be rate limited.` }],
|
|
125
|
+
details: null as any,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const result = computeComps(companies);
|
|
130
|
+
result.unavailableSymbols = unavailableSymbols;
|
|
131
|
+
|
|
132
|
+
const availableSymbols = companies.map((company) => company.symbol);
|
|
133
|
+
const header = `**Comparable Company Analysis**: ${availableSymbols.join(" vs ")}`;
|
|
134
|
+
const rows = result.metrics.map((m) => {
|
|
135
|
+
const vals = availableSymbols.map((s) => {
|
|
136
|
+
const v = m.values[s];
|
|
137
|
+
if (v == null) return "N/A".padStart(10);
|
|
138
|
+
if (Math.abs(v) < 1) return `${(v * 100).toFixed(1)}%`.padStart(10);
|
|
139
|
+
return v.toFixed(2).padStart(10);
|
|
140
|
+
});
|
|
141
|
+
const medStr = m.median != null
|
|
142
|
+
? (Math.abs(m.median) < 1 ? `${(m.median * 100).toFixed(1)}%` : m.median.toFixed(2))
|
|
143
|
+
: "N/A";
|
|
144
|
+
return ` ${m.metric.padEnd(16)} ${vals.join("")} Med: ${medStr} Best: ${m.best}`;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const symHeader = ` ${"Metric".padEnd(16)} ${availableSymbols.map((s) => s.padStart(10)).join("")}`;
|
|
148
|
+
const noteLines = unavailableSymbols.length > 0
|
|
149
|
+
? ["", `Unavailable fundamentals: ${unavailableSymbols.join(", ")}`]
|
|
150
|
+
: [];
|
|
151
|
+
const text = [header, "", symHeader, ...rows, ...noteLines].join("\n");
|
|
152
|
+
|
|
153
|
+
return { content: [{ type: "text", text }], details: result };
|
|
154
|
+
});
|
|
155
|
+
},
|
|
156
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getOverview, getFinancials } from "../../providers/alpha-vantage.js";
|
|
4
|
+
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
5
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
|
+
import { getConfig } from "../../config.js";
|
|
7
|
+
import { withCredentialCheck } from "../../onboarding/tool-helpers.js";
|
|
8
|
+
import type { FinancialStatement } from "../../types/fundamentals.js";
|
|
9
|
+
|
|
10
|
+
export interface DCFResult {
|
|
11
|
+
intrinsicValue: number;
|
|
12
|
+
enterpriseValue: number;
|
|
13
|
+
terminalValue: number;
|
|
14
|
+
netDebt: number;
|
|
15
|
+
projectedCashFlows: Array<{ year: number; fcf: number; presentValue: number }>;
|
|
16
|
+
assumptions: {
|
|
17
|
+
fcf: number;
|
|
18
|
+
growthRate: number;
|
|
19
|
+
discountRate: number;
|
|
20
|
+
terminalGrowth: number;
|
|
21
|
+
years: number;
|
|
22
|
+
};
|
|
23
|
+
sensitivityTable: Array<{ growthRate: number; discountRate: number; intrinsicValue: number }>;
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DCFParams {
|
|
28
|
+
freeCashFlow: number;
|
|
29
|
+
growthRate: number;
|
|
30
|
+
discountRate: number;
|
|
31
|
+
terminalGrowth: number;
|
|
32
|
+
years: number;
|
|
33
|
+
netDebt: number;
|
|
34
|
+
sharesOutstanding: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function computeDCF(params: DCFParams): DCFResult {
|
|
38
|
+
const { freeCashFlow, growthRate, discountRate, terminalGrowth, years, netDebt, sharesOutstanding } = params;
|
|
39
|
+
|
|
40
|
+
// Project future cash flows (mid-year convention: discount at year-0.5)
|
|
41
|
+
const projectedCashFlows: Array<{ year: number; fcf: number; presentValue: number }> = [];
|
|
42
|
+
for (let y = 1; y <= years; y++) {
|
|
43
|
+
const fcf = freeCashFlow * (1 + growthRate) ** y;
|
|
44
|
+
const pv = fcf / (1 + discountRate) ** (y - 0.5); // mid-year convention
|
|
45
|
+
projectedCashFlows.push({ year: y, fcf, presentValue: pv });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Terminal value (Gordon Growth Model), discounted at full year (end of projection)
|
|
49
|
+
const finalFCF = freeCashFlow * (1 + growthRate) ** years;
|
|
50
|
+
const terminalValue = (finalFCF * (1 + terminalGrowth)) / (discountRate - terminalGrowth);
|
|
51
|
+
const pvTerminal = terminalValue / (1 + discountRate) ** years;
|
|
52
|
+
|
|
53
|
+
// Enterprise value
|
|
54
|
+
const sumPVs = projectedCashFlows.reduce((s, cf) => s + cf.presentValue, 0);
|
|
55
|
+
const enterpriseValue = sumPVs + pvTerminal;
|
|
56
|
+
|
|
57
|
+
// Equity value → per share
|
|
58
|
+
const equityValue = enterpriseValue - netDebt;
|
|
59
|
+
const intrinsicValue = equityValue / sharesOutstanding;
|
|
60
|
+
|
|
61
|
+
// Sensitivity table: vary growth ±2% and discount ±2%
|
|
62
|
+
const sensitivityTable: Array<{ growthRate: number; discountRate: number; intrinsicValue: number }> = [];
|
|
63
|
+
for (let gDelta = -0.02; gDelta <= 0.02; gDelta += 0.01) {
|
|
64
|
+
for (let dDelta = -0.02; dDelta <= 0.02; dDelta += 0.01) {
|
|
65
|
+
const g = growthRate + gDelta;
|
|
66
|
+
const d = discountRate + dDelta;
|
|
67
|
+
if (d <= terminalGrowth || d <= 0 || g < 0) continue;
|
|
68
|
+
const sensResult = computeDCFSimple(freeCashFlow, g, d, terminalGrowth, years, netDebt, sharesOutstanding);
|
|
69
|
+
sensitivityTable.push({ growthRate: g, discountRate: d, intrinsicValue: sensResult });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Validation warnings (inspired by Anthropic financial plugins + Dexter)
|
|
74
|
+
const warnings: string[] = [];
|
|
75
|
+
const tvPctOfEV = pvTerminal / enterpriseValue;
|
|
76
|
+
if (tvPctOfEV > 0.85) {
|
|
77
|
+
warnings.push(`Terminal value is ${(tvPctOfEV * 100).toFixed(0)}% of enterprise value (typical: 40-80%). The valuation is heavily dependent on terminal assumptions.`);
|
|
78
|
+
}
|
|
79
|
+
const spreadPct = discountRate - terminalGrowth;
|
|
80
|
+
if (spreadPct < 0.02) {
|
|
81
|
+
warnings.push(`Terminal growth (${(terminalGrowth * 100).toFixed(1)}%) is very close to discount rate (${(discountRate * 100).toFixed(1)}%). Small changes in assumptions will produce large swings in value.`);
|
|
82
|
+
}
|
|
83
|
+
if (discountRate < 0.05 || discountRate > 0.20) {
|
|
84
|
+
warnings.push(`Discount rate of ${(discountRate * 100).toFixed(1)}% is outside typical WACC range (5-20%).`);
|
|
85
|
+
}
|
|
86
|
+
if (growthRate > 0.20) {
|
|
87
|
+
warnings.push(`Growth rate of ${(growthRate * 100).toFixed(1)}% exceeds 20%. High growth is difficult to sustain — consider a multi-stage model.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
intrinsicValue,
|
|
92
|
+
enterpriseValue,
|
|
93
|
+
terminalValue,
|
|
94
|
+
netDebt,
|
|
95
|
+
projectedCashFlows,
|
|
96
|
+
assumptions: {
|
|
97
|
+
fcf: freeCashFlow,
|
|
98
|
+
growthRate,
|
|
99
|
+
discountRate,
|
|
100
|
+
terminalGrowth,
|
|
101
|
+
years,
|
|
102
|
+
},
|
|
103
|
+
sensitivityTable,
|
|
104
|
+
warnings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function computeDCFSimple(
|
|
109
|
+
fcf: number, g: number, d: number, tg: number, years: number, debt: number, shares: number,
|
|
110
|
+
): number {
|
|
111
|
+
let sumPV = 0;
|
|
112
|
+
for (let y = 1; y <= years; y++) {
|
|
113
|
+
sumPV += (fcf * (1 + g) ** y) / (1 + d) ** (y - 0.5); // mid-year convention
|
|
114
|
+
}
|
|
115
|
+
const finalFCF = fcf * (1 + g) ** years;
|
|
116
|
+
const tv = (finalFCF * (1 + tg)) / (d - tg);
|
|
117
|
+
const pvTV = tv / (1 + d) ** years;
|
|
118
|
+
return (sumPV + pvTV - debt) / shares;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function computeNetDebt(f: FinancialStatement): number {
|
|
122
|
+
if (f.totalDebt != null && f.cashAndEquivalents != null) {
|
|
123
|
+
return f.totalDebt - f.cashAndEquivalents;
|
|
124
|
+
}
|
|
125
|
+
// Fallback: totalLiabilities - totalAssets (negative means net cash position)
|
|
126
|
+
return f.totalLiabilities - f.totalAssets;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const params = Type.Object({
|
|
130
|
+
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT)" }),
|
|
131
|
+
growth_rate: Type.Optional(
|
|
132
|
+
Type.Number({ description: "Annual FCF growth rate as decimal (e.g. 0.10 for 10%). If omitted, estimated from historical data." }),
|
|
133
|
+
),
|
|
134
|
+
discount_rate: Type.Optional(
|
|
135
|
+
Type.Number({ description: "Discount rate / WACC as decimal (default: 0.10 for 10%)" }),
|
|
136
|
+
),
|
|
137
|
+
terminal_growth: Type.Optional(
|
|
138
|
+
Type.Number({ description: "Terminal growth rate as decimal (default: 0.03 for 3%)" }),
|
|
139
|
+
),
|
|
140
|
+
projection_years: Type.Optional(
|
|
141
|
+
Type.Number({ description: "Years to project forward (default: 5)" }),
|
|
142
|
+
),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const dcfTool: AgentTool<typeof params> = {
|
|
146
|
+
name: "compute_dcf",
|
|
147
|
+
label: "DCF Valuation",
|
|
148
|
+
description:
|
|
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
|
+
parameters: params,
|
|
151
|
+
async execute(toolCallId, args) {
|
|
152
|
+
return withCredentialCheck("alpha_vantage", async () => {
|
|
153
|
+
const symbol = args.symbol.toUpperCase();
|
|
154
|
+
const config = getConfig();
|
|
155
|
+
|
|
156
|
+
const [overviewResult, financialsResult, quoteResult] = await Promise.all([
|
|
157
|
+
wrapProvider("alphavantage", () => getOverview(symbol, config.alphaVantageApiKey!)),
|
|
158
|
+
wrapProvider("alphavantage", () => getFinancials(symbol, config.alphaVantageApiKey!)),
|
|
159
|
+
wrapProvider("yahoo", () => getQuote(symbol)),
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const missing: string[] = [];
|
|
163
|
+
if (overviewResult.status === "unavailable") missing.push(`company overview (${overviewResult.reason})`);
|
|
164
|
+
if (financialsResult.status === "unavailable") missing.push(`financial statements (${financialsResult.reason})`);
|
|
165
|
+
if (quoteResult.status === "unavailable") missing.push(`stock quote (${quoteResult.reason})`);
|
|
166
|
+
|
|
167
|
+
if (financialsResult.status === "unavailable" || quoteResult.status === "unavailable") {
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: `⚠ DCF valuation unavailable for ${symbol}. Missing: ${missing.join(", ")}. Both financials and current price are required.` }],
|
|
170
|
+
details: null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const overview = overviewResult.status === "ok" ? overviewResult.data : null;
|
|
175
|
+
const financials = financialsResult.data;
|
|
176
|
+
const quote = quoteResult.data;
|
|
177
|
+
|
|
178
|
+
const latestFCF = financials[0]?.freeCashFlow ?? 0;
|
|
179
|
+
if (latestFCF <= 0) {
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: "text", text: `${symbol} has negative or zero free cash flow ($${latestFCF.toLocaleString()}). DCF is not meaningful for companies without positive FCF.` }],
|
|
182
|
+
details: null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Estimate growth from historical FCF if not provided
|
|
187
|
+
let growthRate = args.growth_rate ?? 0.10;
|
|
188
|
+
if (!args.growth_rate && financials.length >= 2) {
|
|
189
|
+
const olderFCF = financials[1]?.freeCashFlow;
|
|
190
|
+
if (olderFCF && olderFCF > 0) {
|
|
191
|
+
growthRate = Math.max(0.02, Math.min(0.25, (latestFCF - olderFCF) / olderFCF));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const discountRate = args.discount_rate ?? 0.10;
|
|
196
|
+
const terminalGrowth = args.terminal_growth ?? 0.03;
|
|
197
|
+
const years = args.projection_years ?? 5;
|
|
198
|
+
const marketCap = overview?.marketCap ?? 0;
|
|
199
|
+
const sharesOutstanding = quote.price > 0 && marketCap > 0 ? marketCap / quote.price : 1;
|
|
200
|
+
const netDebt = financials[0] ? computeNetDebt(financials[0]) : 0;
|
|
201
|
+
|
|
202
|
+
const result = computeDCF({
|
|
203
|
+
freeCashFlow: latestFCF,
|
|
204
|
+
growthRate,
|
|
205
|
+
discountRate,
|
|
206
|
+
terminalGrowth,
|
|
207
|
+
years,
|
|
208
|
+
netDebt: Math.max(0, netDebt),
|
|
209
|
+
sharesOutstanding,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const marginOfSafety = (result.intrinsicValue - quote.price) / result.intrinsicValue;
|
|
213
|
+
const upside = (result.intrinsicValue - quote.price) / quote.price;
|
|
214
|
+
|
|
215
|
+
const lines = [
|
|
216
|
+
`**${symbol} DCF Valuation**`,
|
|
217
|
+
``,
|
|
218
|
+
`Current Price: $${quote.price.toFixed(2)}`,
|
|
219
|
+
`Intrinsic Value: $${result.intrinsicValue.toFixed(2)}`,
|
|
220
|
+
`Margin of Safety: ${(marginOfSafety * 100).toFixed(1)}%`,
|
|
221
|
+
`Upside/Downside: ${upside >= 0 ? "+" : ""}${(upside * 100).toFixed(1)}%`,
|
|
222
|
+
``,
|
|
223
|
+
`**Assumptions**`,
|
|
224
|
+
`Free Cash Flow: $${(latestFCF / 1e9).toFixed(2)}B`,
|
|
225
|
+
`Growth Rate: ${(growthRate * 100).toFixed(1)}%`,
|
|
226
|
+
`Discount Rate (WACC): ${(discountRate * 100).toFixed(1)}%`,
|
|
227
|
+
`Terminal Growth: ${(terminalGrowth * 100).toFixed(1)}%`,
|
|
228
|
+
`Projection: ${years} years`,
|
|
229
|
+
``,
|
|
230
|
+
`**Projected Cash Flows**`,
|
|
231
|
+
...result.projectedCashFlows.map((cf) =>
|
|
232
|
+
` Year ${cf.year}: FCF $${(cf.fcf / 1e9).toFixed(2)}B → PV $${(cf.presentValue / 1e9).toFixed(2)}B`
|
|
233
|
+
),
|
|
234
|
+
` Terminal Value: $${(result.terminalValue / 1e9).toFixed(2)}B`,
|
|
235
|
+
` Enterprise Value: $${(result.enterpriseValue / 1e9).toFixed(2)}B`,
|
|
236
|
+
``,
|
|
237
|
+
`**Sensitivity Table** (Intrinsic Value at different Growth/Discount rates)`,
|
|
238
|
+
...formatSensitivityTable(result.sensitivityTable),
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
243
|
+
details: { ...result, currentPrice: quote.price, marginOfSafety, upside },
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
function formatSensitivityTable(
|
|
250
|
+
table: Array<{ growthRate: number; discountRate: number; intrinsicValue: number }>,
|
|
251
|
+
): string[] {
|
|
252
|
+
if (table.length === 0) return [" (insufficient data for sensitivity table)"];
|
|
253
|
+
|
|
254
|
+
const discountRates = [...new Set(table.map((e) => e.discountRate))].sort((a, b) => a - b);
|
|
255
|
+
const growthRates = [...new Set(table.map((e) => e.growthRate))].sort((a, b) => a - b);
|
|
256
|
+
|
|
257
|
+
const header = ` ${"Growth↓/WACC→".padEnd(14)} ${discountRates.map((d) => `${(d * 100).toFixed(0)}%`.padStart(8)).join("")}`;
|
|
258
|
+
const rows = growthRates.map((g) => {
|
|
259
|
+
const cells = discountRates.map((d) => {
|
|
260
|
+
const entry = table.find((e) => e.growthRate === g && e.discountRate === d);
|
|
261
|
+
return entry ? `$${entry.intrinsicValue.toFixed(0)}`.padStart(8) : "N/A".padStart(8);
|
|
262
|
+
});
|
|
263
|
+
return ` ${`${(g * 100).toFixed(0)}%`.padEnd(14)} ${cells.join("")}`;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return [header, ...rows];
|
|
267
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getEarnings } from "../../providers/alpha-vantage.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import { getConfig } from "../../config.js";
|
|
6
|
+
import { withCredentialCheck } from "../../onboarding/tool-helpers.js";
|
|
7
|
+
import type { EarningsData } from "../../types/fundamentals.js";
|
|
8
|
+
|
|
9
|
+
const params = Type.Object({
|
|
10
|
+
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT)" }),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const earningsTool: AgentTool<typeof params, EarningsData | { credentialRequired: unknown }> = {
|
|
14
|
+
name: "get_earnings",
|
|
15
|
+
label: "Earnings History",
|
|
16
|
+
description:
|
|
17
|
+
"Get quarterly earnings: reported EPS, estimated EPS, and surprise percentage for the last 8 quarters. Requires Alpha Vantage.",
|
|
18
|
+
parameters: params,
|
|
19
|
+
async execute(toolCallId, args) {
|
|
20
|
+
return withCredentialCheck("alpha_vantage", async () => {
|
|
21
|
+
const apiKey = getConfig().alphaVantageApiKey!;
|
|
22
|
+
const result = await wrapProvider("alphavantage", () => getEarnings(args.symbol.toUpperCase(), apiKey));
|
|
23
|
+
if (result.status === "unavailable") {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: `⚠ Earnings data unavailable for ${args.symbol.toUpperCase()} (${result.reason}). Analysis will proceed without earnings history.` }],
|
|
26
|
+
details: null as any,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const earnings = result.data;
|
|
30
|
+
if (earnings.quarterly.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: `No earnings data found for ${args.symbol}` }],
|
|
33
|
+
details: earnings,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const header = `${args.symbol.toUpperCase()} — Quarterly Earnings (last ${earnings.quarterly.length} quarters)`;
|
|
38
|
+
const rows = earnings.quarterly.map((q) => {
|
|
39
|
+
const sign = q.surprisePercent >= 0 ? "+" : "";
|
|
40
|
+
return `${q.date} | Reported: $${q.reportedEPS.toFixed(2)} | Est: $${q.estimatedEPS.toFixed(2)} | Surprise: ${sign}${q.surprisePercent.toFixed(1)}%`;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const text = [header, ...rows].join("\n");
|
|
44
|
+
return { content: [{ type: "text", text }], details: earnings };
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|