opencandle 0.3.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.
- package/LICENSE +1 -1
- package/README.md +106 -14
- package/assets/logo.svg +187 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +11 -3
- package/dist/infra/browser.js +2 -1
- 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/infra/rate-limiter.d.ts +4 -0
- package/dist/infra/rate-limiter.js +5 -1
- package/dist/infra/rate-limiter.js.map +1 -1
- 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/manager.d.ts +9 -0
- package/dist/memory/manager.js +28 -11
- package/dist/memory/manager.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 +7 -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/memory/types.js +4 -0
- package/dist/memory/types.js.map +1 -1
- 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 +391 -21
- 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 +11 -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 +40 -3
- package/dist/prompts/context-builder.js +140 -19
- 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/policy-cards.d.ts +13 -0
- package/dist/prompts/policy-cards.js +197 -0
- package/dist/prompts/policy-cards.js.map +1 -0
- package/dist/prompts/sections.js +3 -3
- package/dist/prompts/sections.js.map +1 -1
- package/dist/prompts/workflow-prompts.d.ts +8 -0
- package/dist/prompts/workflow-prompts.js +208 -22
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/alpha-vantage.js +23 -1
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/sec-edgar.d.ts +8 -1
- package/dist/providers/sec-edgar.js +172 -5
- package/dist/providers/sec-edgar.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +2 -0
- package/dist/providers/yahoo-finance.js +203 -35
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.d.ts +3 -0
- package/dist/routing/classify-intent.js +82 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/defaults.js +4 -4
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/entity-extractor.d.ts +1 -0
- package/dist/routing/entity-extractor.js +158 -12
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/index.d.ts +10 -0
- package/dist/routing/index.js +7 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/legacy-rule-router.d.ts +9 -0
- package/dist/routing/legacy-rule-router.js +12 -0
- package/dist/routing/legacy-rule-router.js.map +1 -0
- package/dist/routing/planning.d.ts +54 -0
- package/dist/routing/planning.js +531 -0
- package/dist/routing/planning.js.map +1 -0
- package/dist/routing/route-manifest.d.ts +35 -0
- package/dist/routing/route-manifest.js +221 -0
- package/dist/routing/route-manifest.js.map +1 -0
- 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 +141 -0
- package/dist/routing/router-prompt.js.map +1 -0
- package/dist/routing/router-types.d.ts +71 -0
- package/dist/routing/router-types.js +2 -0
- package/dist/routing/router-types.js.map +1 -0
- package/dist/routing/router.d.ts +11 -0
- package/dist/routing/router.js +638 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/routing/slot-resolver.js +46 -6
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/routing/turn-context.d.ts +44 -0
- package/dist/routing/turn-context.js +45 -0
- package/dist/routing/turn-context.js.map +1 -0
- package/dist/routing/types.d.ts +13 -1
- package/dist/runtime/answer-contracts.d.ts +82 -0
- package/dist/runtime/answer-contracts.js +414 -0
- package/dist/runtime/answer-contracts.js.map +1 -0
- package/dist/runtime/artifact-contracts.d.ts +14 -0
- package/dist/runtime/artifact-contracts.js +57 -0
- package/dist/runtime/artifact-contracts.js.map +1 -0
- package/dist/runtime/planning-evidence.d.ts +99 -0
- package/dist/runtime/planning-evidence.js +445 -0
- package/dist/runtime/planning-evidence.js.map +1 -0
- package/dist/runtime/session-coordinator.d.ts +81 -3
- package/dist/runtime/session-coordinator.js +201 -17
- 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 +23 -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/company-overview.js +1 -1
- package/dist/tools/fundamentals/company-overview.js.map +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -1
- package/dist/tools/fundamentals/comps.js +1 -1
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +1 -1
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +1 -1
- package/dist/tools/fundamentals/earnings.js +1 -1
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.d.ts +1 -1
- package/dist/tools/fundamentals/financials.js +1 -1
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +2 -1
- package/dist/tools/fundamentals/sec-filings.js +19 -2
- package/dist/tools/fundamentals/sec-filings.js.map +1 -1
- package/dist/tools/index.d.ts +29 -1
- package/dist/tools/index.js +30 -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/fear-greed.js +1 -1
- package/dist/tools/macro/fear-greed.js.map +1 -1
- package/dist/tools/macro/fred-data.d.ts +1 -1
- package/dist/tools/macro/fred-data.js +29 -5
- package/dist/tools/macro/fred-data.js.map +1 -1
- package/dist/tools/market/crypto-history.d.ts +1 -1
- package/dist/tools/market/crypto-history.js +18 -2
- package/dist/tools/market/crypto-history.js.map +1 -1
- package/dist/tools/market/crypto-price.d.ts +1 -1
- package/dist/tools/market/crypto-price.js +1 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/search-ticker.d.ts +1 -1
- package/dist/tools/market/search-ticker.js +1 -1
- package/dist/tools/market/search-ticker.js.map +1 -1
- package/dist/tools/market/stock-history.d.ts +1 -1
- package/dist/tools/market/stock-history.js +1 -1
- package/dist/tools/market/stock-history.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +1 -1
- package/dist/tools/market/stock-quote.js +1 -1
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/greeks.js +0 -1
- package/dist/tools/options/greeks.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +1 -1
- package/dist/tools/options/option-chain.js +13 -5
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/correlation.js +1 -1
- package/dist/tools/portfolio/correlation.js.map +1 -1
- package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
- package/dist/tools/portfolio/holdings-overlap.js +105 -0
- package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
- package/dist/tools/portfolio/predictions.d.ts +1 -1
- package/dist/tools/portfolio/predictions.js +1 -1
- package/dist/tools/portfolio/predictions.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/portfolio/tracker.d.ts +1 -1
- package/dist/tools/portfolio/tracker.js +1 -1
- package/dist/tools/portfolio/tracker.js.map +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +1 -1
- package/dist/tools/portfolio/watchlist.js +12 -4
- package/dist/tools/portfolio/watchlist.js.map +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-summary.js +57 -2
- package/dist/tools/sentiment/sentiment-summary.js.map +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/twitter-sentiment.js +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-search.d.ts +1 -1
- package/dist/tools/sentiment/web-search.js +32 -3
- package/dist/tools/sentiment/web-search.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/web-sentiment.js +1 -1
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +3 -3
- package/dist/tools/technical/backtest.js +41 -27
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.d.ts +1 -1
- package/dist/tools/technical/indicators.js +8 -4
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/options.d.ts +10 -0
- package/dist/types/portfolio.d.ts +27 -0
- package/dist/workflows/compare-assets.js +38 -2
- package/dist/workflows/compare-assets.js.map +1 -1
- package/dist/workflows/options-screener.js +94 -8
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +9 -5
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/ask-user-bridge.ts +82 -0
- package/gui/server/background-quotes.ts +31 -0
- package/gui/server/chat-event-adapter.ts +142 -0
- package/gui/server/gui-session-manager.ts +5 -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 +254 -0
- package/gui/server/prompt-observation.ts +61 -0
- package/gui/server/server.ts +703 -0
- package/gui/server/session-actions.ts +31 -0
- package/gui/server/session-entry-wait.ts +81 -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-Bmp6Knu7.js +1 -0
- package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
- package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
- package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
- package/gui/web/dist/index.html +17 -0
- package/package.json +50 -18
- package/src/analysts/contracts.ts +189 -0
- package/src/analysts/orchestrator.ts +300 -0
- package/src/cli.ts +206 -0
- package/src/config.ts +245 -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 +73 -0
- package/src/memory/index.ts +10 -0
- package/src/memory/manager.ts +192 -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 +205 -0
- package/src/memory/tool-defaults.ts +87 -0
- package/src/memory/types.ts +71 -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 +955 -0
- package/src/pi/session-storage.ts +5 -0
- package/src/pi/session.ts +81 -0
- package/src/pi/setup.ts +381 -0
- package/src/pi/tool-adapter.ts +36 -0
- package/src/prompts/context-builder.ts +315 -0
- package/src/prompts/disclaimer.ts +9 -0
- package/src/prompts/policy-cards.ts +220 -0
- package/src/prompts/sections.ts +46 -0
- package/src/prompts/workflow-prompts.ts +433 -0
- package/src/providers/alpha-vantage.ts +315 -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 +312 -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 +534 -0
- package/src/routing/classify-intent.ts +285 -0
- package/src/routing/defaults.ts +29 -0
- package/src/routing/entity-extractor.ts +291 -0
- package/src/routing/index.ts +70 -0
- package/src/routing/legacy-rule-router.ts +13 -0
- package/src/routing/planning.ts +732 -0
- package/src/routing/route-manifest.ts +287 -0
- package/src/routing/router-llm-client.ts +51 -0
- package/src/routing/router-prompt.ts +163 -0
- package/src/routing/router-types.ts +87 -0
- package/src/routing/router.ts +712 -0
- package/src/routing/slot-resolver.ts +190 -0
- package/src/routing/turn-context.ts +111 -0
- package/src/routing/types.ts +75 -0
- package/src/runtime/answer-contracts.ts +633 -0
- package/src/runtime/artifact-contracts.ts +76 -0
- package/src/runtime/evidence.ts +77 -0
- package/src/runtime/planning-evidence.ts +591 -0
- package/src/runtime/prompt-step.ts +75 -0
- package/src/runtime/provider-tracker.ts +40 -0
- package/src/runtime/run-context.ts +22 -0
- package/src/runtime/session-coordinator.ts +472 -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 +118 -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 +84 -0
- package/src/tools/index.ts +91 -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 +80 -0
- package/src/tools/market/crypto-history.ts +67 -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 +81 -0
- package/src/tools/options/option-chain.ts +96 -0
- package/src/tools/portfolio/correlation.ts +162 -0
- package/src/tools/portfolio/holdings-overlap.ts +123 -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 +159 -0
- package/src/tools/sentiment/reddit-sentiment.ts +164 -0
- package/src/tools/sentiment/sentiment-summary.ts +316 -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 +183 -0
- package/src/tools/sentiment/web-sentiment.ts +76 -0
- package/src/tools/technical/backtest.ts +267 -0
- package/src/tools/technical/indicators.ts +256 -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 +52 -0
- package/src/types/portfolio.ts +73 -0
- package/src/types/sentiment.ts +70 -0
- package/src/workflows/compare-assets.ts +75 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/options-screener.ts +127 -0
- package/src/workflows/portfolio-builder.ts +56 -0
- package/src/workflows/types.ts +4 -0
- package/dist/runtime/index.d.ts +0 -16
- package/dist/runtime/index.js +0 -10
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/provider-ids.d.ts +0 -14
- package/dist/runtime/provider-ids.js +0 -14
- package/dist/runtime/provider-ids.js.map +0 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getOptionsChain } from "../../providers/yahoo-finance.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import type { OptionsChain, OptionContract } from "../../types/options.js";
|
|
6
|
+
|
|
7
|
+
const params = Type.Object({
|
|
8
|
+
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, TSLA, SPY, MSFT)" }),
|
|
9
|
+
expiration: Type.Optional(
|
|
10
|
+
Type.String({
|
|
11
|
+
description:
|
|
12
|
+
"Expiration date as YYYY-MM-DD. If omitted, uses the nearest expiration.",
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
type: Type.Optional(
|
|
16
|
+
Type.Union([Type.Literal("call"), Type.Literal("put"), Type.Literal("CALL"), Type.Literal("PUT")], {
|
|
17
|
+
description: "Filter by option type. Omit for both calls and puts.",
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const optionChainTool: AgentTool<typeof params, OptionsChain> = {
|
|
23
|
+
name: "get_option_chain",
|
|
24
|
+
label: "Options Chain",
|
|
25
|
+
description:
|
|
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
|
+
parameters: params,
|
|
28
|
+
async execute(_toolCallId, args) {
|
|
29
|
+
const symbol = args.symbol.toUpperCase();
|
|
30
|
+
const normalizedType = args.type?.toLowerCase();
|
|
31
|
+
const expirationTs = args.expiration
|
|
32
|
+
? Math.floor(new Date(args.expiration).getTime() / 1000)
|
|
33
|
+
: undefined;
|
|
34
|
+
|
|
35
|
+
const result = await wrapProvider("yahoo", () => getOptionsChain(symbol, expirationTs));
|
|
36
|
+
if (result.status === "unavailable") {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: `⚠ Options chain unavailable for ${symbol} (${result.reason}).` }],
|
|
39
|
+
details: null as any,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const chain = result.data;
|
|
43
|
+
|
|
44
|
+
const lines: string[] = [
|
|
45
|
+
`**${chain.symbol} Options Chain** — Expiry: ${chain.expirationDate}`,
|
|
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
|
+
: []),
|
|
52
|
+
`Available expirations: ${formatAvailableExpirations(chain.expirationDates)}`,
|
|
53
|
+
"",
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const showCalls = !normalizedType || normalizedType === "call";
|
|
57
|
+
const showPuts = !normalizedType || normalizedType === "put";
|
|
58
|
+
|
|
59
|
+
if (showCalls && chain.calls.length > 0) {
|
|
60
|
+
lines.push(`**CALLS** (${chain.calls.length} contracts, volume: ${chain.totalCallVolume.toLocaleString()})`);
|
|
61
|
+
lines.push("Strike | Bid/Ask (per share) | Last (per share) | Vol | OI | IV | Delta | Gamma | Theta | Vega | Rho");
|
|
62
|
+
const topCalls = sortByVolume(chain.calls).slice(0, 10);
|
|
63
|
+
for (const c of topCalls) {
|
|
64
|
+
lines.push(formatContract(c));
|
|
65
|
+
}
|
|
66
|
+
lines.push("");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (showPuts && chain.puts.length > 0) {
|
|
70
|
+
lines.push(`**PUTS** (${chain.puts.length} contracts, volume: ${chain.totalPutVolume.toLocaleString()})`);
|
|
71
|
+
lines.push("Strike | Bid/Ask (per share) | Last (per share) | Vol | OI | IV | Delta | Gamma | Theta | Vega | Rho");
|
|
72
|
+
const topPuts = sortByVolume(chain.puts).slice(0, 10);
|
|
73
|
+
for (const c of topPuts) {
|
|
74
|
+
lines.push(formatContract(c));
|
|
75
|
+
}
|
|
76
|
+
lines.push("");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lines.push(`Put/Call Ratio: ${chain.putCallRatio.toFixed(2)}`);
|
|
80
|
+
|
|
81
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: chain };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function sortByVolume(contracts: OptionContract[]): OptionContract[] {
|
|
86
|
+
return [...contracts].sort((a, b) => b.volume - a.volume);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatAvailableExpirations(expirationDates: string[]): string {
|
|
90
|
+
return expirationDates.join(", ");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatContract(c: OptionContract): string {
|
|
94
|
+
const itm = c.inTheMoney ? "*" : " ";
|
|
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)}`;
|
|
96
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getHistory } from "../../providers/yahoo-finance.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import { computeDailyReturns } from "./risk-analysis.js";
|
|
6
|
+
import type { OHLCV } from "../../types/market.js";
|
|
7
|
+
|
|
8
|
+
export function computeCorrelation(returnsA: number[], returnsB: number[]): number {
|
|
9
|
+
const n = Math.min(returnsA.length, returnsB.length);
|
|
10
|
+
if (n === 0) return 0;
|
|
11
|
+
|
|
12
|
+
let sumA = 0, sumB = 0;
|
|
13
|
+
for (let i = 0; i < n; i++) {
|
|
14
|
+
sumA += returnsA[i];
|
|
15
|
+
sumB += returnsB[i];
|
|
16
|
+
}
|
|
17
|
+
const meanA = sumA / n;
|
|
18
|
+
const meanB = sumB / n;
|
|
19
|
+
|
|
20
|
+
let cov = 0, varA = 0, varB = 0;
|
|
21
|
+
for (let i = 0; i < n; i++) {
|
|
22
|
+
const dA = returnsA[i] - meanA;
|
|
23
|
+
const dB = returnsB[i] - meanB;
|
|
24
|
+
cov += dA * dB;
|
|
25
|
+
varA += dA * dA;
|
|
26
|
+
varB += dB * dB;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (varA === 0 || varB === 0) return 0;
|
|
30
|
+
return cov / Math.sqrt(varA * varB);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_MIN_OVERLAP = 20;
|
|
34
|
+
|
|
35
|
+
export function alignReturnsByDate(
|
|
36
|
+
historiesBySymbol: Map<string, OHLCV[]>,
|
|
37
|
+
minOverlap: number = DEFAULT_MIN_OVERLAP,
|
|
38
|
+
): Map<string, number[]> {
|
|
39
|
+
// Build date → close price maps for each symbol
|
|
40
|
+
const priceByDate = new Map<string, Map<string, number>>();
|
|
41
|
+
for (const [symbol, bars] of historiesBySymbol) {
|
|
42
|
+
const dateMap = new Map<string, number>();
|
|
43
|
+
for (const bar of bars) {
|
|
44
|
+
dateMap.set(bar.date, bar.close);
|
|
45
|
+
}
|
|
46
|
+
priceByDate.set(symbol, dateMap);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Find common dates across all symbols
|
|
50
|
+
const symbols = [...historiesBySymbol.keys()];
|
|
51
|
+
const firstDates = priceByDate.get(symbols[0])!;
|
|
52
|
+
const commonDates = [...firstDates.keys()].filter((date) =>
|
|
53
|
+
symbols.every((s) => priceByDate.get(s)!.has(date)),
|
|
54
|
+
).sort();
|
|
55
|
+
|
|
56
|
+
if (commonDates.length < minOverlap) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Insufficient date overlap for correlation: ${commonDates.length} common dates (need ${minOverlap}+). Symbols may trade on different exchanges or have sparse history.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Extract aligned close prices, then compute returns
|
|
63
|
+
const result = new Map<string, number[]>();
|
|
64
|
+
for (const symbol of symbols) {
|
|
65
|
+
const dateMap = priceByDate.get(symbol)!;
|
|
66
|
+
const alignedCloses = commonDates.map((d) => dateMap.get(d)!);
|
|
67
|
+
result.set(symbol, computeDailyReturns(alignedCloses));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const params = Type.Object({
|
|
74
|
+
symbols: Type.Array(Type.String(), {
|
|
75
|
+
description: "Array of 2+ ticker symbols to compute correlation matrix (e.g. ['AAPL','MSFT','GOOGL'])",
|
|
76
|
+
minItems: 2,
|
|
77
|
+
}),
|
|
78
|
+
period: Type.Optional(
|
|
79
|
+
Type.String({ description: "Historical period: 6mo, 1y, 2y. Default: 1y" }),
|
|
80
|
+
),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const correlationTool: AgentTool<typeof params> = {
|
|
84
|
+
name: "analyze_correlation",
|
|
85
|
+
label: "Correlation Matrix",
|
|
86
|
+
description:
|
|
87
|
+
"Compute pairwise return correlations between 2+ stocks. Identifies highly correlated positions (|r| > 0.7) as concentration risk. Useful for portfolio diversification analysis.",
|
|
88
|
+
parameters: params,
|
|
89
|
+
async execute(_toolCallId, args) {
|
|
90
|
+
const symbols = args.symbols.map((s) => s.toUpperCase());
|
|
91
|
+
const period = args.period ?? "1y";
|
|
92
|
+
|
|
93
|
+
if (symbols.length < 2) {
|
|
94
|
+
throw new Error("Need at least 2 symbols for correlation analysis.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Fetch history for all symbols in parallel
|
|
98
|
+
const results = await Promise.all(
|
|
99
|
+
symbols.map(async (s) => ({
|
|
100
|
+
symbol: s,
|
|
101
|
+
result: await wrapProvider("yahoo", () => getHistory(s, period, "1d")),
|
|
102
|
+
})),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const unavailable = results.filter((r) => r.result.status === "unavailable");
|
|
106
|
+
if (unavailable.length === results.length) {
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text: `⚠ Correlation analysis unavailable — could not fetch history for any symbol.` }],
|
|
109
|
+
details: null as any,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const historiesBySymbol = new Map<string, OHLCV[]>();
|
|
114
|
+
for (const { symbol: sym, result: r } of results) {
|
|
115
|
+
if (r.status === "ok") historiesBySymbol.set(sym, r.data);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const returnsBySymbol = alignReturnsByDate(historiesBySymbol);
|
|
119
|
+
|
|
120
|
+
// Build correlation matrix
|
|
121
|
+
const matrix: Record<string, Record<string, number>> = {};
|
|
122
|
+
const warnings: string[] = [];
|
|
123
|
+
|
|
124
|
+
for (const a of symbols) {
|
|
125
|
+
matrix[a] = {};
|
|
126
|
+
for (const b of symbols) {
|
|
127
|
+
if (a === b) {
|
|
128
|
+
matrix[a][b] = 1.0;
|
|
129
|
+
} else if (matrix[b]?.[a] != null) {
|
|
130
|
+
matrix[a][b] = matrix[b][a];
|
|
131
|
+
} else {
|
|
132
|
+
const r = computeCorrelation(returnsBySymbol.get(a)!, returnsBySymbol.get(b)!);
|
|
133
|
+
matrix[a][b] = r;
|
|
134
|
+
if (Math.abs(r) > 0.7 && a < b) {
|
|
135
|
+
warnings.push(`${a}/${b}: r=${r.toFixed(2)} — high correlation, concentration risk`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Format output
|
|
142
|
+
const header = `**Correlation Matrix** (${period} daily returns)`;
|
|
143
|
+
const colHeader = `${"".padEnd(8)} ${symbols.map((s) => s.padStart(8)).join("")}`;
|
|
144
|
+
const rows = symbols.map((a) => {
|
|
145
|
+
const cells = symbols.map((b) => matrix[a][b].toFixed(2).padStart(8));
|
|
146
|
+
return `${a.padEnd(8)} ${cells.join("")}`;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const lines = [header, "", colHeader, ...rows];
|
|
150
|
+
if (warnings.length > 0) {
|
|
151
|
+
lines.push("", "**Concentration Warnings:**");
|
|
152
|
+
for (const w of warnings) lines.push(` - ${w}`);
|
|
153
|
+
} else {
|
|
154
|
+
lines.push("", "No high-correlation pairs detected. Portfolio appears diversified.");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
159
|
+
details: { matrix, warnings },
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getFundHoldings } from "../../providers/yahoo-finance.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import type {
|
|
6
|
+
FundHolding,
|
|
7
|
+
FundHoldings,
|
|
8
|
+
FundHoldingsOverlap,
|
|
9
|
+
FundOverlapPair,
|
|
10
|
+
SharedFundHolding,
|
|
11
|
+
} from "../../types/portfolio.js";
|
|
12
|
+
|
|
13
|
+
const params = Type.Object({
|
|
14
|
+
symbols: Type.Array(Type.String(), {
|
|
15
|
+
description: "Array of 2+ ETF or fund ticker symbols to compare holdings overlap, e.g. ['VOO','QQQ']",
|
|
16
|
+
minItems: 2,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const holdingsOverlapTool: AgentTool<typeof params, FundHoldingsOverlap | null> = {
|
|
21
|
+
name: "analyze_holdings_overlap",
|
|
22
|
+
label: "ETF Holdings Overlap",
|
|
23
|
+
description:
|
|
24
|
+
"Fetch top fund/ETF holdings and compute pairwise overlap by weight. Useful for ETF diversification and hidden concentration checks.",
|
|
25
|
+
parameters: params,
|
|
26
|
+
async execute(_toolCallId, args) {
|
|
27
|
+
const symbols = [...new Set(args.symbols.map((symbol) => symbol.toUpperCase()))];
|
|
28
|
+
if (symbols.length < 2) {
|
|
29
|
+
throw new Error("Need at least 2 symbols for holdings overlap analysis.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const results = await Promise.all(symbols.map(async (symbol) => ({
|
|
33
|
+
symbol,
|
|
34
|
+
result: await wrapProvider("yahoo", () => getFundHoldings(symbol)),
|
|
35
|
+
})));
|
|
36
|
+
const unavailable = results.flatMap((entry) =>
|
|
37
|
+
entry.result.status === "unavailable"
|
|
38
|
+
? [{ symbol: entry.symbol, reason: entry.result.reason }]
|
|
39
|
+
: []
|
|
40
|
+
);
|
|
41
|
+
if (unavailable.length > 0) {
|
|
42
|
+
const missing = unavailable.map((entry) => `${entry.symbol}: ${entry.reason}`).join("; ");
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: `⚠ Holdings overlap unavailable for one or more funds (${missing}).` }],
|
|
45
|
+
details: null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const funds = results.flatMap((entry) => entry.result.status === "ok" ? [entry.result.data] : []);
|
|
50
|
+
const overlap = computeHoldingsOverlap(funds);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: formatOverlap(overlap) }],
|
|
53
|
+
details: overlap,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function computeHoldingsOverlap(funds: FundHoldings[]): FundHoldingsOverlap {
|
|
59
|
+
const pairs: FundOverlapPair[] = [];
|
|
60
|
+
for (let i = 0; i < funds.length; i += 1) {
|
|
61
|
+
for (let j = i + 1; j < funds.length; j += 1) {
|
|
62
|
+
pairs.push(computePairOverlap(funds[i], funds[j]));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { funds, pairs };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function computePairOverlap(a: FundHoldings, b: FundHoldings): FundOverlapPair {
|
|
69
|
+
const aBySymbol = new Map(a.holdings.map((holding) => [holding.symbol, holding]));
|
|
70
|
+
const sharedHoldings: SharedFundHolding[] = [];
|
|
71
|
+
for (const bHolding of b.holdings) {
|
|
72
|
+
const aHolding = aBySymbol.get(bHolding.symbol);
|
|
73
|
+
if (!aHolding) continue;
|
|
74
|
+
const overlapWeight = roundWeight(Math.min(aHolding.weight, bHolding.weight));
|
|
75
|
+
sharedHoldings.push({
|
|
76
|
+
symbol: bHolding.symbol,
|
|
77
|
+
name: commonHoldingName(aHolding, bHolding),
|
|
78
|
+
weights: {
|
|
79
|
+
[a.symbol]: aHolding.weight,
|
|
80
|
+
[b.symbol]: bHolding.weight,
|
|
81
|
+
},
|
|
82
|
+
overlapWeight,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
sharedHoldings.sort((left, right) => right.overlapWeight - left.overlapWeight);
|
|
86
|
+
return {
|
|
87
|
+
symbols: [a.symbol, b.symbol],
|
|
88
|
+
overlapWeight: roundWeight(sharedHoldings.reduce((sum, holding) => sum + holding.overlapWeight, 0)),
|
|
89
|
+
sharedHoldings,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatOverlap(overlap: FundHoldingsOverlap): string {
|
|
94
|
+
const lines: string[] = ["**ETF/Fund Holdings Overlap**", ""];
|
|
95
|
+
for (const pair of overlap.pairs) {
|
|
96
|
+
lines.push(`${pair.symbols.join("/")} holdings overlap: ${formatPercent(pair.overlapWeight)}`);
|
|
97
|
+
const topShared = pair.sharedHoldings.slice(0, 5);
|
|
98
|
+
if (topShared.length > 0) {
|
|
99
|
+
lines.push(`Top shared holdings: ${topShared.map((holding) =>
|
|
100
|
+
`${holding.symbol} (${formatPercent(holding.overlapWeight)} overlap)`
|
|
101
|
+
).join(", ")}`);
|
|
102
|
+
} else {
|
|
103
|
+
lines.push("No shared top holdings found in provider coverage.");
|
|
104
|
+
}
|
|
105
|
+
lines.push("");
|
|
106
|
+
}
|
|
107
|
+
lines.push("Provider note: overlap uses provider top-holdings coverage, not full portfolio look-through unless the provider returns all holdings.");
|
|
108
|
+
return lines.join("\n").trimEnd();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function commonHoldingName(a: FundHolding, b: FundHolding): string {
|
|
112
|
+
if (a.name && a.name !== a.symbol) return a.name;
|
|
113
|
+
if (b.name && b.name !== b.symbol) return b.name;
|
|
114
|
+
return a.symbol;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatPercent(value: number): string {
|
|
118
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function roundWeight(value: number): number {
|
|
122
|
+
return Math.round(value * 10_000) / 10_000;
|
|
123
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
5
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
|
+
import { ensureParentDir, getPredictionsPath } from "../../infra/opencandle-paths.js";
|
|
7
|
+
|
|
8
|
+
export interface Prediction {
|
|
9
|
+
symbol: string;
|
|
10
|
+
direction: "bullish" | "bearish" | "neutral";
|
|
11
|
+
conviction: number; // 1-10
|
|
12
|
+
entryPrice: number;
|
|
13
|
+
targetPrice?: number;
|
|
14
|
+
date: string;
|
|
15
|
+
expiresAt: string;
|
|
16
|
+
timeframeDays: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PredictionCheckResult {
|
|
20
|
+
total: number;
|
|
21
|
+
open: number;
|
|
22
|
+
correct: number;
|
|
23
|
+
wrong: number;
|
|
24
|
+
hitRate: number;
|
|
25
|
+
weightedHitRate: number;
|
|
26
|
+
details: Array<{
|
|
27
|
+
symbol: string;
|
|
28
|
+
direction: string;
|
|
29
|
+
conviction: number;
|
|
30
|
+
entryPrice: number;
|
|
31
|
+
currentPrice: number;
|
|
32
|
+
pnlPercent: number;
|
|
33
|
+
correct: boolean;
|
|
34
|
+
status: "open" | "resolved";
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadPredictions(): Prediction[] {
|
|
39
|
+
const predictionsPath = getPredictionsPath();
|
|
40
|
+
if (!existsSync(predictionsPath)) return [];
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(readFileSync(predictionsPath, "utf-8"));
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function savePredictions(predictions: Prediction[]): void {
|
|
49
|
+
const predictionsPath = getPredictionsPath();
|
|
50
|
+
ensureParentDir(predictionsPath);
|
|
51
|
+
writeFileSync(predictionsPath, JSON.stringify(predictions, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function recordPrediction(params: {
|
|
55
|
+
symbol: string;
|
|
56
|
+
direction: "bullish" | "bearish" | "neutral";
|
|
57
|
+
conviction: number;
|
|
58
|
+
entryPrice: number;
|
|
59
|
+
targetPrice?: number;
|
|
60
|
+
timeframeDays: number;
|
|
61
|
+
}): Prediction {
|
|
62
|
+
const predictions = loadPredictions();
|
|
63
|
+
const now = new Date();
|
|
64
|
+
const expires = new Date(now);
|
|
65
|
+
expires.setDate(expires.getDate() + params.timeframeDays);
|
|
66
|
+
|
|
67
|
+
const prediction: Prediction = {
|
|
68
|
+
symbol: params.symbol.toUpperCase(),
|
|
69
|
+
direction: params.direction,
|
|
70
|
+
conviction: params.conviction,
|
|
71
|
+
entryPrice: params.entryPrice,
|
|
72
|
+
targetPrice: params.targetPrice,
|
|
73
|
+
date: now.toISOString().split("T")[0],
|
|
74
|
+
expiresAt: expires.toISOString().split("T")[0],
|
|
75
|
+
timeframeDays: params.timeframeDays,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
predictions.push(prediction);
|
|
79
|
+
savePredictions(predictions);
|
|
80
|
+
return prediction;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function checkPredictions(
|
|
84
|
+
predictions: Prediction[],
|
|
85
|
+
currentPrices: Map<string, number>,
|
|
86
|
+
now: Date = new Date(),
|
|
87
|
+
): PredictionCheckResult {
|
|
88
|
+
if (predictions.length === 0) {
|
|
89
|
+
return { total: 0, open: 0, correct: 0, wrong: 0, hitRate: 0, weightedHitRate: 0, details: [] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const details: PredictionCheckResult["details"] = [];
|
|
93
|
+
let totalConviction = 0;
|
|
94
|
+
let correctConviction = 0;
|
|
95
|
+
let openCount = 0;
|
|
96
|
+
|
|
97
|
+
const nowStr = now.toISOString().split("T")[0];
|
|
98
|
+
|
|
99
|
+
for (const p of predictions) {
|
|
100
|
+
const currentPrice = currentPrices.get(p.symbol);
|
|
101
|
+
if (currentPrice == null) continue;
|
|
102
|
+
|
|
103
|
+
const isExpired = p.expiresAt <= nowStr;
|
|
104
|
+
const pnlPercent = (currentPrice - p.entryPrice) / p.entryPrice;
|
|
105
|
+
|
|
106
|
+
if (!isExpired) {
|
|
107
|
+
openCount++;
|
|
108
|
+
details.push({
|
|
109
|
+
symbol: p.symbol,
|
|
110
|
+
direction: p.direction,
|
|
111
|
+
conviction: p.conviction,
|
|
112
|
+
entryPrice: p.entryPrice,
|
|
113
|
+
currentPrice,
|
|
114
|
+
pnlPercent,
|
|
115
|
+
correct: false,
|
|
116
|
+
status: "open",
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const correct =
|
|
122
|
+
(p.direction === "bullish" && currentPrice > p.entryPrice) ||
|
|
123
|
+
(p.direction === "bearish" && currentPrice < p.entryPrice) ||
|
|
124
|
+
(p.direction === "neutral" && Math.abs(pnlPercent) < 0.02);
|
|
125
|
+
|
|
126
|
+
details.push({
|
|
127
|
+
symbol: p.symbol,
|
|
128
|
+
direction: p.direction,
|
|
129
|
+
conviction: p.conviction,
|
|
130
|
+
entryPrice: p.entryPrice,
|
|
131
|
+
currentPrice,
|
|
132
|
+
pnlPercent,
|
|
133
|
+
correct,
|
|
134
|
+
status: "resolved",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
totalConviction += p.conviction;
|
|
138
|
+
if (correct) correctConviction += p.conviction;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const resolved = details.filter((d) => d.status === "resolved");
|
|
142
|
+
const correctCount = resolved.filter((d) => d.correct).length;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
total: details.length,
|
|
146
|
+
open: openCount,
|
|
147
|
+
correct: correctCount,
|
|
148
|
+
wrong: resolved.length - correctCount,
|
|
149
|
+
hitRate: resolved.length > 0 ? correctCount / resolved.length : 0,
|
|
150
|
+
weightedHitRate: totalConviction > 0 ? correctConviction / totalConviction : 0,
|
|
151
|
+
details,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const params = Type.Object({
|
|
156
|
+
action: Type.Union(
|
|
157
|
+
[Type.Literal("record"), Type.Literal("check")],
|
|
158
|
+
{ description: "record: save a new prediction. check: evaluate all predictions against current prices." },
|
|
159
|
+
),
|
|
160
|
+
symbol: Type.Optional(Type.String({ description: "Ticker symbol (required for record)" })),
|
|
161
|
+
direction: Type.Optional(
|
|
162
|
+
Type.Union(
|
|
163
|
+
[Type.Literal("bullish"), Type.Literal("bearish"), Type.Literal("neutral")],
|
|
164
|
+
{ description: "Predicted direction (required for record)" },
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
conviction: Type.Optional(
|
|
168
|
+
Type.Number({ description: "Conviction 1-10 (required for record)" }),
|
|
169
|
+
),
|
|
170
|
+
entry_price: Type.Optional(
|
|
171
|
+
Type.Number({ description: "Entry price at time of prediction (required for record)" }),
|
|
172
|
+
),
|
|
173
|
+
target_price: Type.Optional(
|
|
174
|
+
Type.Number({ description: "Optional target price" }),
|
|
175
|
+
),
|
|
176
|
+
timeframe_days: Type.Optional(
|
|
177
|
+
Type.Number({ description: "Timeframe in days for the prediction (default: 30)" }),
|
|
178
|
+
),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
export const predictionsTool: AgentTool<typeof params> = {
|
|
182
|
+
name: "track_prediction",
|
|
183
|
+
label: "Prediction Tracker",
|
|
184
|
+
description:
|
|
185
|
+
"Track your analysis predictions and measure accuracy over time. Record: save a directional prediction with conviction. Check: evaluate all predictions against current prices, compute hit rate and conviction-weighted accuracy. Inspired by ATLAS's Darwinian scoring approach.",
|
|
186
|
+
parameters: params,
|
|
187
|
+
async execute(_toolCallId, args) {
|
|
188
|
+
if (args.action === "record") {
|
|
189
|
+
if (!args.symbol || !args.direction || !args.conviction || !args.entry_price) {
|
|
190
|
+
throw new Error("symbol, direction, conviction, and entry_price are required for record action.");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const prediction = recordPrediction({
|
|
194
|
+
symbol: args.symbol,
|
|
195
|
+
direction: args.direction,
|
|
196
|
+
conviction: args.conviction,
|
|
197
|
+
entryPrice: args.entry_price,
|
|
198
|
+
targetPrice: args.target_price,
|
|
199
|
+
timeframeDays: args.timeframe_days ?? 30,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: `Recorded: ${prediction.symbol} ${prediction.direction} (conviction ${prediction.conviction}/10) at $${prediction.entryPrice}. Expires ${prediction.expiresAt}.` }],
|
|
204
|
+
details: prediction,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check action
|
|
209
|
+
const predictions = loadPredictions();
|
|
210
|
+
if (predictions.length === 0) {
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: "No predictions recorded yet. Use record action to track your calls." }],
|
|
213
|
+
details: null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Fetch current prices for all symbols
|
|
218
|
+
const symbols = [...new Set(predictions.map((p) => p.symbol))];
|
|
219
|
+
const priceMap = new Map<string, number>();
|
|
220
|
+
await Promise.all(
|
|
221
|
+
symbols.map(async (sym) => {
|
|
222
|
+
const result = await wrapProvider("yahoo", () => getQuote(sym));
|
|
223
|
+
if (result.status === "ok") {
|
|
224
|
+
priceMap.set(sym, result.data.price);
|
|
225
|
+
} else {
|
|
226
|
+
// Skip symbols that are unavailable
|
|
227
|
+
}
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const result = checkPredictions(predictions, priceMap);
|
|
232
|
+
|
|
233
|
+
const resolved = result.correct + result.wrong;
|
|
234
|
+
const lines = [
|
|
235
|
+
`**Prediction Scorecard** — ${result.total} predictions (${resolved} resolved, ${result.open} open)`,
|
|
236
|
+
``,
|
|
237
|
+
`Hit Rate: ${(result.hitRate * 100).toFixed(0)}% (${result.correct}/${resolved})`,
|
|
238
|
+
`Weighted Hit Rate: ${(result.weightedHitRate * 100).toFixed(0)}% (by conviction)`,
|
|
239
|
+
``,
|
|
240
|
+
...result.details.map((d) => {
|
|
241
|
+
const icon = d.status === "open" ? "~" : d.correct ? "+" : "-";
|
|
242
|
+
const sign = d.pnlPercent >= 0 ? "+" : "";
|
|
243
|
+
const label = d.status === "open" ? " (open)" : "";
|
|
244
|
+
return ` [${icon}] ${d.symbol}: ${d.direction} (conv ${d.conviction}) → $${d.entryPrice.toFixed(2)} → $${d.currentPrice.toFixed(2)} (${sign}${(d.pnlPercent * 100).toFixed(1)}%)${label}`;
|
|
245
|
+
}),
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
250
|
+
details: result,
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
};
|