opencandle 0.2.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/README.md +110 -87
- package/assets/logo.svg +187 -0
- package/dist/analysts/orchestrator.js +1 -2
- package/dist/analysts/orchestrator.js.map +1 -1
- 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 +34 -5
- package/dist/config.js +29 -8
- 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/cache.d.ts +4 -0
- package/dist/infra/cache.js +4 -0
- package/dist/infra/cache.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.js +6 -0
- 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/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 +35 -0
- package/dist/onboarding/connect.js +118 -0
- package/dist/onboarding/connect.js.map +1 -0
- package/dist/onboarding/credential-interceptor.d.ts +44 -0
- package/dist/onboarding/credential-interceptor.js +72 -0
- package/dist/onboarding/credential-interceptor.js.map +1 -0
- package/dist/onboarding/degradation-accumulator.d.ts +21 -0
- package/dist/onboarding/degradation-accumulator.js +55 -0
- package/dist/onboarding/degradation-accumulator.js.map +1 -0
- package/dist/onboarding/prompt-user.d.ts +23 -0
- package/dist/onboarding/prompt-user.js +61 -0
- package/dist/onboarding/prompt-user.js.map +1 -0
- package/dist/onboarding/providers.d.ts +116 -0
- package/dist/onboarding/providers.js +239 -0
- package/dist/onboarding/providers.js.map +1 -0
- package/dist/onboarding/state.d.ts +31 -2
- package/dist/onboarding/state.js +141 -13
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +34 -0
- package/dist/onboarding/tool-helpers.js +80 -0
- package/dist/onboarding/tool-helpers.js.map +1 -0
- package/dist/onboarding/tool-tags.d.ts +37 -0
- package/dist/onboarding/tool-tags.js +149 -0
- package/dist/onboarding/tool-tags.js.map +1 -0
- package/dist/onboarding/validation.d.ts +19 -0
- package/dist/onboarding/validation.js +117 -0
- package/dist/onboarding/validation.js.map +1 -0
- package/dist/pi/opencandle-extension.d.ts +7 -1
- package/dist/pi/opencandle-extension.js +488 -13
- 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 -2
- package/dist/pi/setup.js +67 -120
- 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 +47 -11
- 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/alpha-vantage.js +20 -1
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/exa-search.d.ts +39 -0
- package/dist/providers/exa-search.js +276 -0
- package/dist/providers/exa-search.js.map +1 -0
- package/dist/providers/finnhub.d.ts +17 -0
- package/dist/providers/finnhub.js +94 -0
- package/dist/providers/finnhub.js.map +1 -0
- package/dist/providers/fred.js +13 -1
- package/dist/providers/fred.js.map +1 -1
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/provider-credential-error.d.ts +8 -0
- package/dist/providers/provider-credential-error.js +22 -0
- package/dist/providers/provider-credential-error.js.map +1 -0
- package/dist/providers/reddit.d.ts +8 -0
- package/dist/providers/reddit.js +36 -9
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/twitter.js +2 -8
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/web-search.d.ts +17 -0
- package/dist/providers/web-search.js +224 -0
- package/dist/providers/web-search.js.map +1 -0
- package/dist/providers/wrap-provider.d.ts +7 -0
- package/dist/providers/wrap-provider.js +15 -0
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.js +70 -33
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +22 -0
- package/dist/routing/classify-intent.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 -4
- 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/adapters/finnhub.d.ts +7 -0
- package/dist/sentiment/adapters/finnhub.js +39 -0
- package/dist/sentiment/adapters/finnhub.js.map +1 -0
- package/dist/sentiment/adapters/reddit.d.ts +11 -0
- package/dist/sentiment/adapters/reddit.js +54 -0
- package/dist/sentiment/adapters/reddit.js.map +1 -0
- package/dist/sentiment/adapters/twitter.d.ts +9 -0
- package/dist/sentiment/adapters/twitter.js +32 -0
- package/dist/sentiment/adapters/twitter.js.map +1 -0
- package/dist/sentiment/adapters/web.d.ts +9 -0
- package/dist/sentiment/adapters/web.js +40 -0
- package/dist/sentiment/adapters/web.js.map +1 -0
- package/dist/sentiment/index.d.ts +16 -0
- package/dist/sentiment/index.js +44 -0
- package/dist/sentiment/index.js.map +1 -0
- package/dist/sentiment/keywords.d.ts +2 -0
- package/dist/sentiment/keywords.js +9 -0
- package/dist/sentiment/keywords.js.map +1 -0
- package/dist/sentiment/pipeline.d.ts +9 -0
- package/dist/sentiment/pipeline.js +57 -0
- package/dist/sentiment/pipeline.js.map +1 -0
- package/dist/sentiment/scorer.d.ts +9 -0
- package/dist/sentiment/scorer.js +64 -0
- package/dist/sentiment/scorer.js.map +1 -0
- package/dist/sentiment/store.d.ts +24 -0
- package/dist/sentiment/store.js +182 -0
- package/dist/sentiment/store.js.map +1 -0
- package/dist/sentiment/trends.d.ts +13 -0
- package/dist/sentiment/trends.js +73 -0
- package/dist/sentiment/trends.js.map +1 -0
- package/dist/sentiment/types.d.ts +66 -0
- package/dist/sentiment/types.js +54 -0
- package/dist/sentiment/types.js.map +1 -0
- package/dist/system-prompt.js +29 -13
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +4 -4
- package/dist/tools/fundamentals/company-overview.d.ts +4 -2
- package/dist/tools/fundamentals/company-overview.js +27 -27
- 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 +45 -45
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +82 -82
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +4 -2
- package/dist/tools/fundamentals/earnings.js +25 -25
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.d.ts +4 -2
- package/dist/tools/fundamentals/financials.js +23 -23
- package/dist/tools/fundamentals/financials.js.map +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 +35 -2
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.d.ts +1 -1
- package/dist/tools/interaction/ask-user.js +28 -64
- package/dist/tools/interaction/ask-user.js.map +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 +4 -2
- package/dist/tools/macro/fred-data.js +26 -26
- 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-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 +4 -2
- package/dist/tools/sentiment/reddit-sentiment.js +107 -22
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +7 -0
- package/dist/tools/sentiment/sentiment-summary.js +230 -0
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -0
- package/dist/tools/sentiment/sentiment-trend.d.ts +22 -0
- package/dist/tools/sentiment/sentiment-trend.js +39 -0
- package/dist/tools/sentiment/sentiment-trend.js.map +1 -0
- package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js +17 -0
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-search.d.ts +11 -0
- package/dist/tools/sentiment/web-search.js +115 -0
- package/dist/tools/sentiment/web-search.js.map +1 -0
- package/dist/tools/sentiment/web-sentiment.d.ts +8 -0
- package/dist/tools/sentiment/web-sentiment.js +66 -0
- package/dist/tools/sentiment/web-sentiment.js.map +1 -0
- 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/types/index.d.ts +1 -1
- package/dist/types/sentiment.d.ts +21 -0
- 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 +62 -20
- 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/dist/tools/sentiment/news-sentiment.d.ts +0 -7
- package/dist/tools/sentiment/news-sentiment.js +0 -55
- package/dist/tools/sentiment/news-sentiment.js.map +0 -1
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { httpGet } from "../infra/http-client.js";
|
|
2
|
+
import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
|
|
3
|
+
import { rateLimiter } from "../infra/rate-limiter.js";
|
|
4
|
+
import { StealthBrowser } from "../infra/browser.js";
|
|
5
|
+
import type { StockQuote, OHLCV } from "../types/market.js";
|
|
6
|
+
import type { OptionsChain, OptionContract } from "../types/options.js";
|
|
7
|
+
import { computeGreeks } from "../tools/options/greeks.js";
|
|
8
|
+
|
|
9
|
+
const BASE_URL = "https://query1.finance.yahoo.com/v8/finance/chart";
|
|
10
|
+
|
|
11
|
+
interface YahooChartResponse {
|
|
12
|
+
chart: {
|
|
13
|
+
result: Array<{
|
|
14
|
+
meta: Record<string, any>;
|
|
15
|
+
timestamp: number[];
|
|
16
|
+
indicators: {
|
|
17
|
+
quote: Array<{
|
|
18
|
+
open: number[];
|
|
19
|
+
high: number[];
|
|
20
|
+
low: number[];
|
|
21
|
+
close: number[];
|
|
22
|
+
volume: number[];
|
|
23
|
+
}>;
|
|
24
|
+
adjclose?: Array<{ adjclose: number[] }>;
|
|
25
|
+
};
|
|
26
|
+
}>;
|
|
27
|
+
error?: { code: string; description: string };
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function getQuote(symbol: string): Promise<StockQuote> {
|
|
32
|
+
const cacheKey = `yahoo:quote:${symbol}`;
|
|
33
|
+
const cached = cache.get<StockQuote>(cacheKey);
|
|
34
|
+
if (cached) return cached;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await rateLimiter.acquire("yahoo");
|
|
38
|
+
|
|
39
|
+
const url = `${BASE_URL}/${encodeURIComponent(symbol)}?interval=1d&range=1d`;
|
|
40
|
+
const data = await httpGet<YahooChartResponse>(url, {
|
|
41
|
+
headers: { "User-Agent": "OpenCandle/1.0" },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (data.chart.error) {
|
|
45
|
+
throw new Error(`Yahoo Finance: ${data.chart.error.description}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = data.chart.result[0];
|
|
49
|
+
const meta = result.meta;
|
|
50
|
+
const indicators = result.indicators.quote[0];
|
|
51
|
+
|
|
52
|
+
const price = meta.regularMarketPrice ?? 0;
|
|
53
|
+
const prevClose = meta.chartPreviousClose ?? meta.previousClose ?? price;
|
|
54
|
+
const change = price - prevClose;
|
|
55
|
+
const changePercent = prevClose !== 0 ? (change / prevClose) * 100 : 0;
|
|
56
|
+
|
|
57
|
+
// Open price: try meta first, fall back to indicators
|
|
58
|
+
const open = meta.regularMarketOpen ?? indicators?.open?.[0] ?? price;
|
|
59
|
+
|
|
60
|
+
const quote: StockQuote = {
|
|
61
|
+
symbol: meta.symbol,
|
|
62
|
+
price,
|
|
63
|
+
change,
|
|
64
|
+
changePercent,
|
|
65
|
+
open,
|
|
66
|
+
high: meta.regularMarketDayHigh ?? indicators?.high?.[0] ?? price,
|
|
67
|
+
low: meta.regularMarketDayLow ?? indicators?.low?.[0] ?? price,
|
|
68
|
+
previousClose: prevClose,
|
|
69
|
+
volume: meta.regularMarketVolume ?? 0,
|
|
70
|
+
marketCap: meta.marketCap ?? 0,
|
|
71
|
+
pe: null, // Not in chart endpoint
|
|
72
|
+
week52High: meta.fiftyTwoWeekHigh ?? 0,
|
|
73
|
+
week52Low: meta.fiftyTwoWeekLow ?? 0,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
cache.set(cacheKey, quote, TTL.QUOTE);
|
|
78
|
+
return quote;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const stale = cache.getStale<StockQuote>(cacheKey, STALE_LIMIT.QUOTE);
|
|
81
|
+
if (stale) return stale.value;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getHistory(
|
|
87
|
+
symbol: string,
|
|
88
|
+
range: string = "6mo",
|
|
89
|
+
interval: string = "1d",
|
|
90
|
+
): Promise<OHLCV[]> {
|
|
91
|
+
const cacheKey = `yahoo:history:${symbol}:${range}:${interval}`;
|
|
92
|
+
const cached = cache.get<OHLCV[]>(cacheKey);
|
|
93
|
+
if (cached) return cached;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await rateLimiter.acquire("yahoo");
|
|
97
|
+
|
|
98
|
+
const url = `${BASE_URL}/${encodeURIComponent(symbol)}?interval=${interval}&range=${range}`;
|
|
99
|
+
const data = await httpGet<YahooChartResponse>(url, {
|
|
100
|
+
headers: { "User-Agent": "OpenCandle/1.0" },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (data.chart.error) {
|
|
104
|
+
throw new Error(`Yahoo Finance: ${data.chart.error.description}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = data.chart.result[0];
|
|
108
|
+
const timestamps = result.timestamp;
|
|
109
|
+
const quotes = result.indicators.quote[0];
|
|
110
|
+
|
|
111
|
+
const ohlcv: OHLCV[] = timestamps
|
|
112
|
+
.map((ts, i) => ({
|
|
113
|
+
date: new Date(ts * 1000).toISOString().split("T")[0],
|
|
114
|
+
open: quotes.open[i],
|
|
115
|
+
high: quotes.high[i],
|
|
116
|
+
low: quotes.low[i],
|
|
117
|
+
close: quotes.close[i],
|
|
118
|
+
volume: quotes.volume[i],
|
|
119
|
+
}))
|
|
120
|
+
.filter((bar) => bar.open != null && bar.close != null);
|
|
121
|
+
|
|
122
|
+
cache.set(cacheKey, ohlcv, TTL.HISTORY);
|
|
123
|
+
return ohlcv;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
const stale = cache.getStale<OHLCV[]>(cacheKey, STALE_LIMIT.HISTORY);
|
|
126
|
+
if (stale) return stale.value;
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- Options Chain (v7 API with crumb+cookie auth) ---
|
|
132
|
+
|
|
133
|
+
const BROWSER_UA =
|
|
134
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
135
|
+
|
|
136
|
+
let cachedCrumb: { crumb: string; cookie: string; expiresAt: number } | null = null;
|
|
137
|
+
|
|
138
|
+
export function clearCrumbCache(): void {
|
|
139
|
+
cachedCrumb = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function getYahooCrumb(): Promise<{ crumb: string; cookie: string }> {
|
|
143
|
+
if (cachedCrumb && Date.now() < cachedCrumb.expiresAt) {
|
|
144
|
+
return { crumb: cachedCrumb.crumb, cookie: cachedCrumb.cookie };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Step 1: Hit fc.yahoo.com to get a session cookie
|
|
148
|
+
const cookieRes = await fetch("https://fc.yahoo.com/t", {
|
|
149
|
+
headers: { "User-Agent": BROWSER_UA },
|
|
150
|
+
});
|
|
151
|
+
const setCookie = cookieRes.headers.get("set-cookie") ?? "";
|
|
152
|
+
const cookie = setCookie.split(";")[0]; // Extract just the cookie value
|
|
153
|
+
|
|
154
|
+
// Step 2: Use the cookie to get a crumb
|
|
155
|
+
const crumbRes = await fetch("https://query2.finance.yahoo.com/v1/test/getcrumb", {
|
|
156
|
+
headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
|
|
157
|
+
});
|
|
158
|
+
const crumb = await crumbRes.text();
|
|
159
|
+
|
|
160
|
+
if (!crumb || crumb.includes("Unauthorized")) {
|
|
161
|
+
throw new Error("Failed to acquire Yahoo Finance crumb");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
cachedCrumb = { crumb, cookie, expiresAt: Date.now() + TTL.CRUMB };
|
|
165
|
+
return { crumb, cookie };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface YahooOptionsResponse {
|
|
169
|
+
optionChain: {
|
|
170
|
+
result: Array<{
|
|
171
|
+
underlyingSymbol: string;
|
|
172
|
+
expirationDates: number[];
|
|
173
|
+
strikes: number[];
|
|
174
|
+
quote: Record<string, any>;
|
|
175
|
+
options: Array<{
|
|
176
|
+
expirationDate: number;
|
|
177
|
+
calls: any[];
|
|
178
|
+
puts: any[];
|
|
179
|
+
}>;
|
|
180
|
+
}>;
|
|
181
|
+
error?: any;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function getOptionsChain(
|
|
186
|
+
symbol: string,
|
|
187
|
+
expiration?: number,
|
|
188
|
+
): Promise<OptionsChain> {
|
|
189
|
+
const cacheKey = `yahoo:options:${symbol}:${expiration ?? "nearest"}`;
|
|
190
|
+
const cached = cache.get<OptionsChain>(cacheKey);
|
|
191
|
+
if (cached) return cached;
|
|
192
|
+
|
|
193
|
+
await rateLimiter.acquire("yahoo");
|
|
194
|
+
|
|
195
|
+
const { crumb, cookie } = await getYahooCrumb();
|
|
196
|
+
const dateParam = expiration ? `&date=${expiration}` : "";
|
|
197
|
+
const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
|
|
198
|
+
|
|
199
|
+
let res: Response | null = null;
|
|
200
|
+
let fetchError: unknown;
|
|
201
|
+
try {
|
|
202
|
+
res = await fetch(url, {
|
|
203
|
+
headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
fetchError = error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// On 401 or 429, refresh crumb and retry once
|
|
210
|
+
if (res?.status === 401 || res?.status === 429) {
|
|
211
|
+
try {
|
|
212
|
+
clearCrumbCache();
|
|
213
|
+
const fresh = await getYahooCrumb();
|
|
214
|
+
const retryUrl = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(fresh.crumb)}${dateParam}`;
|
|
215
|
+
res = await fetch(retryUrl, {
|
|
216
|
+
headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
fetchError = error;
|
|
220
|
+
res = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// If still failing, fall back to stealth browser (bypasses TLS fingerprinting)
|
|
225
|
+
if (!res?.ok) {
|
|
226
|
+
let browserError: unknown;
|
|
227
|
+
try {
|
|
228
|
+
const browserData = await fetchOptionsViaBrowser(symbol, expiration);
|
|
229
|
+
if (browserData) {
|
|
230
|
+
const chain = parseOptionsResponse(symbol, browserData);
|
|
231
|
+
cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
|
|
232
|
+
return chain;
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
browserError = error;
|
|
236
|
+
}
|
|
237
|
+
// All fetches failed — try stale cache before giving up
|
|
238
|
+
const stale = cache.getStale<OptionsChain>(cacheKey, STALE_LIMIT.OPTIONS_CHAIN);
|
|
239
|
+
if (stale) return stale.value;
|
|
240
|
+
if (res) {
|
|
241
|
+
const message = `Yahoo Finance options: HTTP ${res.status}`;
|
|
242
|
+
if (browserError instanceof Error) {
|
|
243
|
+
throw new Error(`${message}; browser fallback failed: ${browserError.message}`);
|
|
244
|
+
}
|
|
245
|
+
throw new Error(message);
|
|
246
|
+
}
|
|
247
|
+
if (browserError instanceof Error) {
|
|
248
|
+
const message = fetchError instanceof Error ? fetchError.message : "Yahoo Finance options: fetch failed";
|
|
249
|
+
throw new Error(`${message}; browser fallback failed: ${browserError.message}`);
|
|
250
|
+
}
|
|
251
|
+
throw fetchError instanceof Error ? fetchError : new Error("Yahoo Finance options: fetch failed");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const data: YahooOptionsResponse = await res.json();
|
|
255
|
+
const chain = parseOptionsResponse(symbol, data);
|
|
256
|
+
cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
|
|
257
|
+
return chain;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Compute time to expiry in years from a Yahoo expiration timestamp (midnight UTC).
|
|
262
|
+
* US equity options expire at 4:00 PM ET. During EDT that is 20:00 UTC.
|
|
263
|
+
* We use 21:00 UTC (4 PM EST / 5 PM EDT) as a conservative close offset
|
|
264
|
+
* and apply a floor of ~1 hour to prevent numerical instability near expiry.
|
|
265
|
+
*/
|
|
266
|
+
export function computeTimeToExpiry(expirationTs: number, nowMs: number = Date.now()): number {
|
|
267
|
+
const MARKET_CLOSE_OFFSET_S = 21 * 3600; // 21:00 UTC ≈ 4 PM ET
|
|
268
|
+
const MIN_TIME_YEARS = 1 / (365 * 24); // ~1 hour floor
|
|
269
|
+
const SECONDS_PER_YEAR = 365 * 24 * 3600;
|
|
270
|
+
|
|
271
|
+
const expiryCloseTs = expirationTs + MARKET_CLOSE_OFFSET_S;
|
|
272
|
+
const remainingS = expiryCloseTs - nowMs / 1000;
|
|
273
|
+
|
|
274
|
+
if (remainingS <= 0) return 0;
|
|
275
|
+
return Math.max(MIN_TIME_YEARS, remainingS / SECONDS_PER_YEAR);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): OptionsChain {
|
|
279
|
+
if (data.optionChain.error) {
|
|
280
|
+
throw new Error(`Yahoo Finance options: ${JSON.stringify(data.optionChain.error)}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const result = data.optionChain.result[0];
|
|
284
|
+
const quote = result.quote;
|
|
285
|
+
const underlyingPrice = quote.regularMarketPrice ?? 0;
|
|
286
|
+
const opts = result.options[0];
|
|
287
|
+
const riskFreeRate = 0.05;
|
|
288
|
+
|
|
289
|
+
const expirationTs = opts.expirationDate;
|
|
290
|
+
const expirationDate = new Date(expirationTs * 1000).toISOString().split("T")[0];
|
|
291
|
+
const timeYears = computeTimeToExpiry(expirationTs);
|
|
292
|
+
|
|
293
|
+
const mapContract = (c: any, type: "call" | "put"): OptionContract => {
|
|
294
|
+
const strike = c.strike ?? c.strike?.raw ?? 0;
|
|
295
|
+
const iv = c.impliedVolatility ?? c.impliedVolatility?.raw ?? 0;
|
|
296
|
+
const greeks = computeGreeks({ type, spot: underlyingPrice, strike, timeYears, iv, riskFreeRate });
|
|
297
|
+
return {
|
|
298
|
+
contractSymbol: c.contractSymbol ?? "",
|
|
299
|
+
type,
|
|
300
|
+
strike,
|
|
301
|
+
expiration: expirationDate,
|
|
302
|
+
bid: c.bid ?? c.bid?.raw ?? 0,
|
|
303
|
+
ask: c.ask ?? c.ask?.raw ?? 0,
|
|
304
|
+
lastPrice: c.lastPrice ?? c.lastPrice?.raw ?? 0,
|
|
305
|
+
volume: c.volume ?? c.volume?.raw ?? 0,
|
|
306
|
+
openInterest: c.openInterest ?? c.openInterest?.raw ?? 0,
|
|
307
|
+
impliedVolatility: iv,
|
|
308
|
+
inTheMoney: c.inTheMoney ?? false,
|
|
309
|
+
greeks,
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const calls = (opts.calls ?? []).map((c: any) => mapContract(c, "call"));
|
|
314
|
+
const puts = (opts.puts ?? []).map((c: any) => mapContract(c, "put"));
|
|
315
|
+
const totalCallVolume = calls.reduce((s, c) => s + c.volume, 0);
|
|
316
|
+
const totalPutVolume = puts.reduce((s, c) => s + c.volume, 0);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
symbol: result.underlyingSymbol,
|
|
320
|
+
underlyingPrice,
|
|
321
|
+
expirationDate,
|
|
322
|
+
expirationDates: result.expirationDates.map((ts) => new Date(ts * 1000).toISOString().split("T")[0]),
|
|
323
|
+
calls,
|
|
324
|
+
puts,
|
|
325
|
+
totalCallVolume,
|
|
326
|
+
totalPutVolume,
|
|
327
|
+
putCallRatio: totalCallVolume > 0 ? totalPutVolume / totalCallVolume : 0,
|
|
328
|
+
fetchedAt: new Date().toISOString(),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Fallback: fetch options data via Camoufox stealth browser.
|
|
334
|
+
* Bypasses Yahoo's TLS fingerprinting and rate limiting.
|
|
335
|
+
*/
|
|
336
|
+
async function fetchOptionsViaBrowser(
|
|
337
|
+
symbol: string,
|
|
338
|
+
expiration?: number,
|
|
339
|
+
): Promise<YahooOptionsResponse | null> {
|
|
340
|
+
try {
|
|
341
|
+
// Avoid loading the script-heavy Yahoo Finance homepage: Playwright 1.60
|
|
342
|
+
// can crash on some pageerror payloads emitted by finance.yahoo.com.
|
|
343
|
+
// Navigating directly to Yahoo's JSON endpoints still uses the browser's
|
|
344
|
+
// cookies/TLS fingerprint without requiring cross-origin fetch from page JS.
|
|
345
|
+
const dateParam = expiration ? `&date=${expiration}` : "";
|
|
346
|
+
return await StealthBrowser.run(async (page) => {
|
|
347
|
+
await page.goto("https://query2.finance.yahoo.com/v1/test/getcrumb", {
|
|
348
|
+
waitUntil: "domcontentloaded",
|
|
349
|
+
timeout: 15000,
|
|
350
|
+
});
|
|
351
|
+
const crumb = (await page.locator("body").innerText()).trim();
|
|
352
|
+
if (!crumb) return null;
|
|
353
|
+
|
|
354
|
+
const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
|
|
355
|
+
const response = await page.goto(url, {
|
|
356
|
+
waitUntil: "domcontentloaded",
|
|
357
|
+
timeout: 15000,
|
|
358
|
+
});
|
|
359
|
+
if (!response?.ok()) return null;
|
|
360
|
+
|
|
361
|
+
const text = (await page.locator("body").innerText()).trim();
|
|
362
|
+
return JSON.parse(text) as YahooOptionsResponse;
|
|
363
|
+
});
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { ClassificationResult, WorkflowType, ExtractedEntities } from "./types.js";
|
|
2
|
+
import { extractEntities } from "./entity-extractor.js";
|
|
3
|
+
|
|
4
|
+
interface Rule {
|
|
5
|
+
workflow: WorkflowType;
|
|
6
|
+
test: (input: string, entities: ExtractedEntities) => boolean;
|
|
7
|
+
confidence: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ANALYSIS_PATTERNS = [
|
|
11
|
+
/^\s*analyze\s+\$?([A-Za-z]{1,5})\s*$/i,
|
|
12
|
+
/^\s*full\s+analysis\s+(?:of\s+)?\$?([A-Za-z]{1,5})\s*$/i,
|
|
13
|
+
/^\s*deep\s+dive\s+(?:on\s+)?\$?([A-Za-z]{1,5})\s*$/i,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const RULES: Rule[] = [
|
|
17
|
+
// Exact-match analysis patterns (highest priority)
|
|
18
|
+
{
|
|
19
|
+
workflow: "single_asset_analysis",
|
|
20
|
+
confidence: 1.0,
|
|
21
|
+
test: (input) => ANALYSIS_PATTERNS.some((p) => p.test(input)),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
workflow: "single_asset_analysis",
|
|
25
|
+
confidence: 0.85,
|
|
26
|
+
test: (input, entities) => {
|
|
27
|
+
const lower = input.toLowerCase();
|
|
28
|
+
return (
|
|
29
|
+
entities.symbols.length === 1 &&
|
|
30
|
+
(/\bis\s+\S+\s+(?:attractive|undervalued|overvalued|cheap|expensive)/i.test(lower) ||
|
|
31
|
+
/\bshould\s+i\s+buy\s+\$?[a-z]{1,5}\b/i.test(lower) ||
|
|
32
|
+
/\bwhat\s+do\s+you\s+think\s+(?:of|about)\s+\$?[a-z]{1,5}\b/i.test(lower))
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
// News / event queries — symbol or topic + news keywords
|
|
37
|
+
// Must come before compare_assets to prevent "NVDA and AI chip exports" from
|
|
38
|
+
// matching as a 2-symbol comparison.
|
|
39
|
+
{
|
|
40
|
+
workflow: "general_finance_qa",
|
|
41
|
+
confidence: 0.9,
|
|
42
|
+
test: (input) => {
|
|
43
|
+
const lower = input.toLowerCase();
|
|
44
|
+
const hasNewsKeyword =
|
|
45
|
+
/\bnews\b/.test(lower) ||
|
|
46
|
+
/\blatest\b/.test(lower) ||
|
|
47
|
+
/\brecent\b/.test(lower) ||
|
|
48
|
+
/\bhappening\b/.test(lower) ||
|
|
49
|
+
/\bannouncement/.test(lower) ||
|
|
50
|
+
/\binvestigation\b/.test(lower) ||
|
|
51
|
+
/\bregulat/.test(lower) ||
|
|
52
|
+
/\blawsuit\b/.test(lower) ||
|
|
53
|
+
/\btariff/.test(lower) ||
|
|
54
|
+
/\bexport/.test(lower) ||
|
|
55
|
+
/\bupdate\b/.test(lower);
|
|
56
|
+
return hasNewsKeyword;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
// Options: symbol + option keyword
|
|
60
|
+
{
|
|
61
|
+
workflow: "options_screener",
|
|
62
|
+
confidence: 0.95,
|
|
63
|
+
test: (input, entities) => {
|
|
64
|
+
const lower = input.toLowerCase();
|
|
65
|
+
const hasOptionKeywords =
|
|
66
|
+
/\bcalls?\b/.test(lower) ||
|
|
67
|
+
/\bputs?\b/.test(lower) ||
|
|
68
|
+
/\boption(?:s)?\s*chain\b/.test(lower) ||
|
|
69
|
+
/\boptions?\b/.test(lower);
|
|
70
|
+
return hasOptionKeywords && entities.symbols.length >= 1;
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
// Compare: keyword + 2+ symbols (uppercase)
|
|
74
|
+
{
|
|
75
|
+
workflow: "compare_assets",
|
|
76
|
+
confidence: 0.95,
|
|
77
|
+
test: (input, entities) => {
|
|
78
|
+
const lower = input.toLowerCase();
|
|
79
|
+
const hasCompareKeywords =
|
|
80
|
+
/\bcompare\b/.test(lower) ||
|
|
81
|
+
/\bvs\.?\b/.test(lower) ||
|
|
82
|
+
/\bversus\b/.test(lower) ||
|
|
83
|
+
/\bwhich\s+is\s+better\b/.test(lower);
|
|
84
|
+
return hasCompareKeywords && entities.symbols.length >= 2;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
// Compare: keyword + lowercase tickers ("Compare aapl and msft")
|
|
88
|
+
{
|
|
89
|
+
workflow: "compare_assets",
|
|
90
|
+
confidence: 0.85,
|
|
91
|
+
test: (input) => {
|
|
92
|
+
const lower = input.toLowerCase();
|
|
93
|
+
return /\bcompare\s+[a-z]{1,5}(?:\s*,?\s*(?:and\s+)?[a-z]{1,5})+/.test(lower);
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
// Compare: 2+ uppercase symbols without explicit keyword
|
|
97
|
+
{
|
|
98
|
+
workflow: "compare_assets",
|
|
99
|
+
confidence: 0.8,
|
|
100
|
+
test: (_input, entities) => entities.symbols.length >= 2,
|
|
101
|
+
},
|
|
102
|
+
// Watchlist/tracking: must come before portfolio_builder to catch "show my portfolio"
|
|
103
|
+
{
|
|
104
|
+
workflow: "watchlist_or_tracking",
|
|
105
|
+
confidence: 0.95,
|
|
106
|
+
test: (input) => {
|
|
107
|
+
const lower = input.toLowerCase();
|
|
108
|
+
return (
|
|
109
|
+
/\bwatchlist\b/.test(lower) ||
|
|
110
|
+
/\bprediction/i.test(lower) ||
|
|
111
|
+
/\bshow\s+my\s+portfolio\b/.test(lower) ||
|
|
112
|
+
/\bmy\s+portfolio\b/.test(lower) ||
|
|
113
|
+
/\btrack\b/.test(lower)
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
// Portfolio: budget + invest keyword
|
|
118
|
+
{
|
|
119
|
+
workflow: "portfolio_builder",
|
|
120
|
+
confidence: 0.9,
|
|
121
|
+
test: (input, entities) => {
|
|
122
|
+
const lower = input.toLowerCase();
|
|
123
|
+
return (
|
|
124
|
+
entities.budget !== undefined &&
|
|
125
|
+
(/\binvest\b/.test(lower) ||
|
|
126
|
+
/\bportfolio\b/.test(lower) ||
|
|
127
|
+
/\ballocat/i.test(lower) ||
|
|
128
|
+
/\bposition/i.test(lower) ||
|
|
129
|
+
/\bbuy\b/.test(lower))
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
// Portfolio: keyword-only (no budget required)
|
|
134
|
+
{
|
|
135
|
+
workflow: "portfolio_builder",
|
|
136
|
+
confidence: 0.8,
|
|
137
|
+
test: (input) => {
|
|
138
|
+
const lower = input.toLowerCase();
|
|
139
|
+
return (
|
|
140
|
+
/\bportfolio\b/.test(lower) ||
|
|
141
|
+
/\bwhat\s+should\s+i\s+(?:invest|buy)\b/.test(lower) ||
|
|
142
|
+
/\bbuild\s+(?:me\s+)?a?\s*(?:diversified\s+)?.*portfolio\b/.test(lower) ||
|
|
143
|
+
(/\binvest\s+in\b/.test(lower) && /\bwhat\b/.test(lower))
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
// General finance Q&A
|
|
148
|
+
{
|
|
149
|
+
workflow: "general_finance_qa",
|
|
150
|
+
confidence: 0.85,
|
|
151
|
+
test: (input) => {
|
|
152
|
+
const lower = input.toLowerCase();
|
|
153
|
+
return (
|
|
154
|
+
/^what\s+(?:is|are|does|do)\b/.test(lower) ||
|
|
155
|
+
/^how\s+(?:does|do|is|are)\b/.test(lower) ||
|
|
156
|
+
/^explain\b/.test(lower) ||
|
|
157
|
+
/^define\b/.test(lower) ||
|
|
158
|
+
/\bwhat\s+does\s+\S+\s+mean\b/.test(lower)
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
export function classifyIntent(input: string): ClassificationResult {
|
|
165
|
+
const trimmed = input.trim();
|
|
166
|
+
if (!trimmed) {
|
|
167
|
+
return {
|
|
168
|
+
workflow: "unclassified",
|
|
169
|
+
confidence: 0,
|
|
170
|
+
tier: "rule",
|
|
171
|
+
entities: { symbols: [] },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const entities = extractEntities(trimmed);
|
|
176
|
+
|
|
177
|
+
for (const rule of RULES) {
|
|
178
|
+
if (rule.test(trimmed, entities)) {
|
|
179
|
+
return {
|
|
180
|
+
workflow: rule.workflow,
|
|
181
|
+
confidence: rule.confidence,
|
|
182
|
+
tier: "rule",
|
|
183
|
+
entities,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
workflow: "unclassified",
|
|
190
|
+
confidence: 0,
|
|
191
|
+
tier: "rule",
|
|
192
|
+
entities,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PortfolioSlots, OptionsScreenerSlots } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const PORTFOLIO_DEFAULTS: Omit<PortfolioSlots, "budget"> = {
|
|
4
|
+
riskProfile: "balanced",
|
|
5
|
+
timeHorizon: "1y_plus",
|
|
6
|
+
assetScope: "mixed_etf_and_large_cap_equities",
|
|
7
|
+
positionCount: 4,
|
|
8
|
+
maxSinglePositionPct: 35,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const OPTIONS_SCREENER_DEFAULTS: Omit<OptionsScreenerSlots, "symbol" | "direction"> = {
|
|
12
|
+
dteTarget: "25_to_45_days",
|
|
13
|
+
objective: "balanced_leverage_and_probability",
|
|
14
|
+
moneynessPreference: "atm_to_slightly_otm",
|
|
15
|
+
liquidityMinimum: "high_open_interest_and_tight_spread",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function parseDteTarget(dteTarget: string): { minDays: number; maxDays: number } | null {
|
|
19
|
+
const rangeMatch = dteTarget.match(/^(\d+)_to_(\d+)_days$/);
|
|
20
|
+
if (rangeMatch) {
|
|
21
|
+
return { minDays: parseInt(rangeMatch[1], 10), maxDays: parseInt(rangeMatch[2], 10) };
|
|
22
|
+
}
|
|
23
|
+
const plusMatch = dteTarget.match(/^(\d+)_plus_days$/);
|
|
24
|
+
if (plusMatch) {
|
|
25
|
+
const min = parseInt(plusMatch[1], 10);
|
|
26
|
+
return { minDays: min, maxDays: Math.max(min, 1095) };
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|