opencandle 0.4.0 → 0.6.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 +186 -117
- package/dist/analysts/contracts.d.ts +1 -3
- package/dist/analysts/contracts.js +1 -11
- package/dist/analysts/contracts.js.map +1 -1
- package/dist/analysts/orchestrator.d.ts +1 -3
- package/dist/analysts/orchestrator.js +1 -26
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli.js +32 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -3
- package/dist/config.js +69 -3
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/browser.d.ts +1 -3
- package/dist/infra/browser.js +4 -2
- package/dist/infra/browser.js.map +1 -1
- package/dist/infra/cache.d.ts +8 -11
- package/dist/infra/cache.js +17 -15
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/http-client.d.ts +4 -1
- package/dist/infra/http-client.js +59 -6
- package/dist/infra/http-client.js.map +1 -1
- package/dist/infra/index.d.ts +3 -3
- package/dist/infra/index.js +3 -3
- package/dist/infra/index.js.map +1 -1
- package/dist/infra/native-dependencies.js +2 -2
- package/dist/infra/native-dependencies.js.map +1 -1
- package/dist/infra/node-version.js.map +1 -1
- package/dist/infra/opencandle-paths.d.ts +0 -3
- package/dist/infra/opencandle-paths.js +4 -11
- package/dist/infra/opencandle-paths.js.map +1 -1
- package/dist/infra/rate-limiter.d.ts +4 -0
- package/dist/infra/rate-limiter.js +17 -10
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/alert-conditions.d.ts +34 -0
- package/dist/market-state/alert-conditions.js +23 -0
- package/dist/market-state/alert-conditions.js.map +1 -0
- package/dist/market-state/alert-runner.d.ts +55 -0
- package/dist/market-state/alert-runner.js +634 -0
- package/dist/market-state/alert-runner.js.map +1 -0
- package/dist/market-state/daily-report.d.ts +26 -0
- package/dist/market-state/daily-report.js +179 -0
- package/dist/market-state/daily-report.js.map +1 -0
- package/dist/market-state/local-automation-service.d.ts +25 -0
- package/dist/market-state/local-automation-service.js +119 -0
- package/dist/market-state/local-automation-service.js.map +1 -0
- package/dist/market-state/notification-delivery.d.ts +14 -0
- package/dist/market-state/notification-delivery.js +139 -0
- package/dist/market-state/notification-delivery.js.map +1 -0
- package/dist/market-state/resolve-for-mutation.d.ts +10 -0
- package/dist/market-state/resolve-for-mutation.js +15 -0
- package/dist/market-state/resolve-for-mutation.js.map +1 -0
- package/dist/market-state/resolve.d.ts +14 -0
- package/dist/market-state/resolve.js +89 -0
- package/dist/market-state/resolve.js.map +1 -0
- package/dist/market-state/service.d.ts +527 -0
- package/dist/market-state/service.js +1099 -0
- package/dist/market-state/service.js.map +1 -0
- package/dist/memory/index.d.ts +7 -7
- package/dist/memory/index.js +6 -6
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +39 -22
- package/dist/memory/manager.js.map +1 -1
- package/dist/memory/retrieval.js +7 -4
- package/dist/memory/retrieval.js.map +1 -1
- package/dist/memory/sqlite.js +385 -3
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.d.ts +3 -2
- package/dist/memory/storage.js +1 -2
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.js +64 -28
- package/dist/memory/tool-defaults.js.map +1 -1
- package/dist/memory/types.js +4 -0
- package/dist/memory/types.js.map +1 -1
- package/dist/monitor.d.ts +2 -0
- package/dist/monitor.js +104 -0
- package/dist/monitor.js.map +1 -0
- package/dist/onboarding/connect.js +4 -6
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/credential-interceptor.js +1 -1
- package/dist/onboarding/credential-interceptor.js.map +1 -1
- package/dist/onboarding/degradation-accumulator.js +1 -3
- package/dist/onboarding/degradation-accumulator.js.map +1 -1
- package/dist/onboarding/providers.js +3 -16
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-helpers.js +1 -1
- package/dist/onboarding/tool-helpers.js.map +1 -1
- package/dist/onboarding/tool-tags.js +6 -4
- package/dist/onboarding/tool-tags.js.map +1 -1
- package/dist/onboarding/validation.js +1 -1
- package/dist/onboarding/validation.js.map +1 -1
- package/dist/pi/opencandle-extension.d.ts +8 -0
- package/dist/pi/opencandle-extension.js +637 -59
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session.d.ts +1 -1
- package/dist/pi/session.js +3 -1
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.js +17 -2
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.js +5 -2
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +18 -3
- package/dist/prompts/context-builder.js +117 -18
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/disclaimer.js +1 -1
- package/dist/prompts/disclaimer.js.map +1 -1
- 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.d.ts +1 -1
- package/dist/prompts/sections.js +3 -3
- package/dist/prompts/sections.js.map +1 -1
- package/dist/prompts/symbol-preflight.d.ts +20 -0
- package/dist/prompts/symbol-preflight.js +49 -0
- package/dist/prompts/symbol-preflight.js.map +1 -0
- package/dist/prompts/workflow-prompts.d.ts +1 -1
- package/dist/prompts/workflow-prompts.js +209 -19
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/alpha-vantage.d.ts +1 -1
- package/dist/providers/alpha-vantage.js +49 -8
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -1
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/errors.d.ts +5 -0
- package/dist/providers/errors.js +11 -0
- package/dist/providers/errors.js.map +1 -0
- package/dist/providers/exa-search.d.ts +2 -2
- package/dist/providers/exa-search.js +19 -11
- package/dist/providers/exa-search.js.map +1 -1
- package/dist/providers/fear-greed.js +1 -1
- package/dist/providers/fear-greed.js.map +1 -1
- package/dist/providers/finnhub.js +3 -5
- package/dist/providers/finnhub.js.map +1 -1
- package/dist/providers/fred.js +2 -2
- package/dist/providers/fred.js.map +1 -1
- package/dist/providers/index.d.ts +7 -6
- package/dist/providers/index.js +6 -5
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/reddit.js +2 -2
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/sec-edgar.d.ts +9 -1
- package/dist/providers/sec-edgar.js +181 -6
- package/dist/providers/sec-edgar.js.map +1 -1
- package/dist/providers/tradingview.d.ts +47 -0
- package/dist/providers/tradingview.js +275 -0
- package/dist/providers/tradingview.js.map +1 -0
- package/dist/providers/twitter.js +6 -8
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/web-search.js +26 -12
- package/dist/providers/web-search.js.map +1 -1
- package/dist/providers/with-fallback.js +4 -2
- package/dist/providers/with-fallback.js.map +1 -1
- package/dist/providers/wrap-provider.d.ts +2 -3
- package/dist/providers/wrap-provider.js +14 -8
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -1
- package/dist/providers/yahoo-finance.js +226 -11
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.d.ts +9 -0
- package/dist/routing/classify-intent.js +153 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/defaults.d.ts +1 -1
- package/dist/routing/defaults.js +3 -3
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/entity-extractor.d.ts +2 -0
- package/dist/routing/entity-extractor.js +377 -26
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/fund-symbols.d.ts +2 -0
- package/dist/routing/fund-symbols.js +55 -0
- package/dist/routing/fund-symbols.js.map +1 -0
- package/dist/routing/horizon.d.ts +1 -0
- package/dist/routing/horizon.js +10 -0
- package/dist/routing/horizon.js.map +1 -0
- package/dist/routing/index.d.ts +12 -6
- package/dist/routing/index.js +8 -4
- 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 +562 -0
- package/dist/routing/planning.js.map +1 -0
- package/dist/routing/route-manifest.d.ts +35 -0
- package/dist/routing/route-manifest.js +242 -0
- package/dist/routing/route-manifest.js.map +1 -0
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +46 -45
- package/dist/routing/router-prompt.js.map +1 -1
- package/dist/routing/router-types.d.ts +10 -0
- package/dist/routing/router.d.ts +1 -0
- package/dist/routing/router.js +572 -13
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -1
- package/dist/routing/slot-resolver.js +45 -7
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/routing/symbol-disambiguator.d.ts +11 -0
- package/dist/routing/symbol-disambiguator.js +52 -0
- package/dist/routing/symbol-disambiguator.js.map +1 -0
- 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 +15 -1
- package/dist/runtime/answer-contracts.d.ts +82 -0
- package/dist/runtime/answer-contracts.js +442 -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 +466 -0
- package/dist/runtime/planning-evidence.js.map +1 -0
- package/dist/runtime/prompt-step.d.ts +1 -9
- package/dist/runtime/prompt-step.js +0 -10
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/run-context.d.ts +5 -2
- package/dist/runtime/run-context.js +8 -1
- package/dist/runtime/run-context.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +29 -3
- package/dist/runtime/session-coordinator.js +204 -31
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/session-title.d.ts +14 -0
- package/dist/runtime/session-title.js +50 -0
- package/dist/runtime/session-title.js.map +1 -0
- package/dist/runtime/tool-defaults-wrapper.js +1 -3
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
- package/dist/runtime/validation.js.map +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-runner.d.ts +3 -3
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/runtime/workflow-runner.js.map +1 -1
- package/dist/sentiment/adapters/finnhub.d.ts +1 -1
- package/dist/sentiment/adapters/finnhub.js +6 -1
- package/dist/sentiment/adapters/finnhub.js.map +1 -1
- package/dist/sentiment/adapters/reddit.d.ts +2 -2
- package/dist/sentiment/adapters/twitter.d.ts +1 -1
- package/dist/sentiment/adapters/web.d.ts +1 -1
- package/dist/sentiment/index.d.ts +9 -11
- package/dist/sentiment/index.js +9 -20
- package/dist/sentiment/index.js.map +1 -1
- package/dist/sentiment/keywords.js +26 -4
- package/dist/sentiment/keywords.js.map +1 -1
- package/dist/sentiment/pipeline.d.ts +2 -2
- package/dist/sentiment/pipeline.js +1 -1
- package/dist/sentiment/pipeline.js.map +1 -1
- package/dist/sentiment/scorer.js +1 -1
- package/dist/sentiment/store.d.ts +1 -1
- package/dist/sentiment/store.js +1 -1
- package/dist/sentiment/store.js.map +1 -1
- package/dist/sentiment/trends.d.ts +1 -1
- package/dist/sentiment/trends.js.map +1 -1
- package/dist/sentiment/types.js.map +1 -1
- package/dist/system-prompt.js +7 -3
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +7 -7
- package/dist/tool-kit.js +4 -4
- package/dist/tool-kit.js.map +1 -1
- package/dist/tools/fundamentals/company-overview.js +12 -7
- package/dist/tools/fundamentals/company-overview.js.map +1 -1
- package/dist/tools/fundamentals/comps.js +19 -10
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.js +24 -12
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.js +9 -4
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.js +9 -4
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +1 -0
- package/dist/tools/fundamentals/sec-filings.js +36 -4
- package/dist/tools/fundamentals/sec-filings.js.map +1 -1
- package/dist/tools/index.d.ts +23 -18
- package/dist/tools/index.js +53 -38
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.js +15 -3
- package/dist/tools/interaction/ask-user.js.map +1 -1
- package/dist/tools/interaction/twitter-login.js +13 -3
- package/dist/tools/interaction/twitter-login.js.map +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 +44 -9
- package/dist/tools/macro/fred-data.js.map +1 -1
- package/dist/tools/market/crypto-history.js +21 -3
- package/dist/tools/market/crypto-history.js.map +1 -1
- package/dist/tools/market/crypto-price.js +4 -2
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.d.ts +18 -0
- package/dist/tools/market/screen-stocks.js +252 -0
- package/dist/tools/market/screen-stocks.js.map +1 -0
- package/dist/tools/market/search-ticker.js +161 -9
- package/dist/tools/market/search-ticker.js.map +1 -1
- package/dist/tools/market/stock-history.d.ts +2 -2
- package/dist/tools/market/stock-history.js +27 -8
- package/dist/tools/market/stock-history.js.map +1 -1
- package/dist/tools/market/stock-quote.js +6 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/greeks.js +1 -2
- package/dist/tools/options/greeks.js.map +1 -1
- package/dist/tools/options/option-chain.js +27 -9
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/alerts.d.ts +15 -0
- package/dist/tools/portfolio/alerts.js +357 -0
- package/dist/tools/portfolio/alerts.js.map +1 -0
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/correlation.js +34 -14
- package/dist/tools/portfolio/correlation.js.map +1 -1
- package/dist/tools/portfolio/daily-report.d.ts +8 -0
- package/dist/tools/portfolio/daily-report.js +83 -0
- package/dist/tools/portfolio/daily-report.js.map +1 -0
- package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
- package/dist/tools/portfolio/holdings-overlap.js +112 -0
- package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
- package/dist/tools/portfolio/notifications.d.ts +7 -0
- package/dist/tools/portfolio/notifications.js +43 -0
- package/dist/tools/portfolio/notifications.js.map +1 -0
- package/dist/tools/portfolio/predictions.d.ts +12 -6
- package/dist/tools/portfolio/predictions.js +338 -88
- 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 +46 -7
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/portfolio/tracker.d.ts +4 -3
- package/dist/tools/portfolio/tracker.js +247 -102
- package/dist/tools/portfolio/tracker.js.map +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +6 -4
- package/dist/tools/portfolio/watchlist.js +209 -101
- package/dist/tools/portfolio/watchlist.js.map +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js +24 -11
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.js +71 -14
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-trend.js +12 -2
- package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js +13 -6
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
- package/dist/tools/sentiment/untrusted-text.js +17 -0
- package/dist/tools/sentiment/untrusted-text.js.map +1 -0
- package/dist/tools/sentiment/web-search.js +37 -12
- package/dist/tools/sentiment/web-search.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.js +16 -4
- 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 +65 -44
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +24 -8
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/market.d.ts +1 -0
- package/dist/types/options.d.ts +10 -0
- package/dist/types/portfolio.d.ts +41 -4
- package/dist/workflows/compare-assets.d.ts +0 -3
- package/dist/workflows/compare-assets.js +55 -10
- package/dist/workflows/compare-assets.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -4
- package/dist/workflows/index.js +3 -3
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/options-screener.d.ts +0 -3
- package/dist/workflows/options-screener.js +88 -14
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.d.ts +0 -3
- package/dist/workflows/portfolio-builder.js +7 -11
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/ask-user-bridge.ts +82 -0
- package/gui/server/automation-heartbeat.ts +97 -0
- package/gui/server/background-quotes.ts +97 -1
- package/gui/server/chat-event-adapter.ts +32 -10
- package/gui/server/chat-run-session.ts +16 -0
- package/gui/server/gui-session-manager.ts +5 -0
- package/gui/server/invoke-tool.ts +144 -1
- package/gui/server/live-chat-event-adapter.ts +21 -6
- package/gui/server/market-state-api.ts +315 -0
- package/gui/server/model-setup.ts +149 -2
- package/gui/server/private-api-access.ts +62 -0
- package/gui/server/projector.ts +58 -11
- package/gui/server/prompt-observation.ts +58 -0
- package/gui/server/quote-snapshot-store.ts +50 -0
- package/gui/server/server.ts +236 -376
- package/gui/server/session-actions.ts +186 -1
- package/gui/server/session-entry-wait.ts +81 -0
- package/gui/server/shutdown.ts +47 -0
- package/gui/server/tool-invoke-ack.ts +49 -0
- package/gui/server/tool-metadata.ts +23 -10
- package/gui/server/websocket.ts +13 -3
- package/gui/server/writer-lock.ts +6 -2
- package/gui/server/ws-hub.ts +292 -0
- package/gui/shared/chat-events.ts +16 -1
- package/gui/shared/event-reducer.ts +24 -6
- package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +1 -0
- package/gui/web/dist/assets/index-2KZtKBmu.css +1 -0
- package/gui/web/dist/assets/index-CveNgtDg.js +69 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +22 -12
- package/src/analysts/contracts.ts +10 -23
- package/src/analysts/orchestrator.ts +8 -43
- package/src/cli.ts +37 -13
- package/src/config.ts +99 -7
- package/src/index.ts +1 -1
- package/src/infra/browser.ts +4 -2
- package/src/infra/cache.ts +41 -30
- package/src/infra/http-client.ts +72 -6
- package/src/infra/index.ts +7 -10
- package/src/infra/native-dependencies.ts +8 -3
- package/src/infra/node-version.ts +3 -1
- package/src/infra/opencandle-paths.ts +3 -14
- package/src/infra/rate-limiter.ts +32 -20
- package/src/market-state/alert-conditions.ts +82 -0
- package/src/market-state/alert-runner.ts +863 -0
- package/src/market-state/daily-report.ts +247 -0
- package/src/market-state/local-automation-service.ts +162 -0
- package/src/market-state/notification-delivery.ts +158 -0
- package/src/market-state/resolve-for-mutation.ts +24 -0
- package/src/market-state/resolve.ts +112 -0
- package/src/market-state/service.ts +2344 -0
- package/src/memory/index.ts +7 -7
- package/src/memory/manager.ts +57 -26
- package/src/memory/retrieval.ts +8 -7
- package/src/memory/sqlite.ts +407 -6
- package/src/memory/storage.ts +8 -17
- package/src/memory/tool-defaults.ts +60 -39
- package/src/memory/types.ts +7 -3
- package/src/monitor.ts +121 -0
- package/src/onboarding/connect.ts +10 -33
- package/src/onboarding/credential-interceptor.ts +3 -15
- package/src/onboarding/degradation-accumulator.ts +1 -3
- package/src/onboarding/providers.ts +9 -40
- package/src/onboarding/state.ts +4 -15
- package/src/onboarding/tool-helpers.ts +2 -9
- package/src/onboarding/tool-tags.ts +6 -6
- package/src/onboarding/validation.ts +14 -20
- package/src/pi/opencandle-extension.ts +795 -120
- package/src/pi/session.ts +7 -5
- package/src/pi/setup.ts +61 -33
- package/src/pi/tool-adapter.ts +5 -2
- package/src/prompts/context-builder.ts +143 -21
- package/src/prompts/disclaimer.ts +1 -1
- package/src/prompts/policy-cards.ts +220 -0
- package/src/prompts/sections.ts +4 -4
- package/src/prompts/symbol-preflight.ts +80 -0
- package/src/prompts/workflow-prompts.ts +231 -28
- package/src/providers/alpha-vantage.ts +82 -40
- package/src/providers/coingecko.ts +2 -5
- package/src/providers/errors.ts +9 -0
- package/src/providers/exa-search.ts +24 -22
- package/src/providers/fear-greed.ts +1 -1
- package/src/providers/finnhub.ts +7 -6
- package/src/providers/fred.ts +3 -3
- package/src/providers/index.ts +14 -6
- package/src/providers/reddit.ts +17 -6
- package/src/providers/sec-edgar.ts +235 -5
- package/src/providers/tradingview.ts +399 -0
- package/src/providers/twitter.ts +6 -8
- package/src/providers/web-search.ts +30 -20
- package/src/providers/with-fallback.ts +8 -7
- package/src/providers/wrap-provider.ts +15 -10
- package/src/providers/yahoo-finance.ts +292 -20
- package/src/routing/classify-intent.ts +186 -4
- package/src/routing/defaults.ts +4 -4
- package/src/routing/entity-extractor.ts +428 -28
- package/src/routing/fund-symbols.ts +58 -0
- package/src/routing/horizon.ts +7 -0
- package/src/routing/index.ts +60 -16
- package/src/routing/legacy-rule-router.ts +13 -0
- package/src/routing/planning.ts +823 -0
- package/src/routing/route-manifest.ts +309 -0
- package/src/routing/router-llm-client.ts +4 -4
- package/src/routing/router-prompt.ts +52 -52
- package/src/routing/router-types.ts +18 -0
- package/src/routing/router.ts +717 -20
- package/src/routing/slot-resolver.ts +75 -14
- package/src/routing/symbol-disambiguator.ts +72 -0
- package/src/routing/turn-context.ts +108 -0
- package/src/routing/types.ts +15 -1
- package/src/runtime/answer-contracts.ts +672 -0
- package/src/runtime/artifact-contracts.ts +77 -0
- package/src/runtime/planning-evidence.ts +682 -0
- package/src/runtime/prompt-step.ts +1 -16
- package/src/runtime/run-context.ts +12 -2
- package/src/runtime/session-coordinator.ts +297 -56
- package/src/runtime/session-title.ts +60 -0
- package/src/runtime/tool-defaults-wrapper.ts +1 -3
- package/src/runtime/validation.ts +1 -4
- package/src/runtime/workflow-events.ts +7 -7
- package/src/runtime/workflow-runner.ts +5 -11
- package/src/sentiment/adapters/finnhub.ts +7 -2
- package/src/sentiment/adapters/reddit.ts +2 -2
- package/src/sentiment/adapters/twitter.ts +1 -1
- package/src/sentiment/adapters/web.ts +1 -1
- package/src/sentiment/index.ts +16 -26
- package/src/sentiment/keywords.ts +26 -4
- package/src/sentiment/pipeline.ts +15 -4
- package/src/sentiment/scorer.ts +1 -1
- package/src/sentiment/store.ts +2 -2
- package/src/sentiment/trends.ts +9 -3
- package/src/sentiment/types.ts +5 -4
- package/src/system-prompt.ts +7 -3
- package/src/tool-kit.ts +10 -9
- package/src/tools/fundamentals/company-overview.ts +20 -10
- package/src/tools/fundamentals/comps.ts +69 -56
- package/src/tools/fundamentals/dcf.ts +146 -96
- package/src/tools/fundamentals/earnings.ts +17 -7
- package/src/tools/fundamentals/financials.ts +17 -8
- package/src/tools/fundamentals/sec-filings.ts +52 -8
- package/src/tools/index.ts +53 -38
- package/src/tools/interaction/ask-user.ts +22 -10
- package/src/tools/interaction/twitter-login.ts +17 -5
- package/src/tools/macro/fear-greed.ts +2 -2
- package/src/tools/macro/fred-data.ts +80 -42
- package/src/tools/market/crypto-history.ts +25 -4
- package/src/tools/market/crypto-price.ts +7 -7
- package/src/tools/market/screen-stocks.ts +279 -0
- package/src/tools/market/search-ticker.ts +219 -18
- package/src/tools/market/stock-history.ts +38 -13
- package/src/tools/market/stock-quote.ts +11 -8
- package/src/tools/options/greeks.ts +5 -6
- package/src/tools/options/option-chain.ts +47 -18
- package/src/tools/portfolio/alerts.ts +457 -0
- package/src/tools/portfolio/correlation.ts +48 -21
- package/src/tools/portfolio/daily-report.ts +101 -0
- package/src/tools/portfolio/holdings-overlap.ts +139 -0
- package/src/tools/portfolio/notifications.ts +45 -0
- package/src/tools/portfolio/predictions.ts +407 -107
- package/src/tools/portfolio/risk-analysis.ts +47 -8
- package/src/tools/portfolio/tracker.ts +271 -110
- package/src/tools/portfolio/watchlist.ts +251 -116
- package/src/tools/sentiment/reddit-sentiment.ts +51 -25
- package/src/tools/sentiment/sentiment-summary.ts +116 -35
- package/src/tools/sentiment/sentiment-trend.ts +24 -7
- package/src/tools/sentiment/twitter-sentiment.ts +23 -16
- package/src/tools/sentiment/untrusted-text.ts +21 -0
- package/src/tools/sentiment/web-search.ts +52 -16
- package/src/tools/sentiment/web-sentiment.ts +27 -11
- package/src/tools/technical/backtest.ts +78 -47
- package/src/tools/technical/indicators.ts +40 -17
- package/src/types/index.ts +8 -3
- package/src/types/market.ts +1 -0
- package/src/types/options.ts +17 -0
- package/src/types/portfolio.ts +46 -4
- package/src/types/sentiment.ts +2 -2
- package/src/workflows/compare-assets.ts +67 -19
- package/src/workflows/index.ts +3 -4
- package/src/workflows/options-screener.ts +98 -22
- package/src/workflows/portfolio-builder.ts +40 -29
- 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
- package/dist/workflows/types.d.ts +0 -4
- package/dist/workflows/types.js +0 -2
- package/dist/workflows/types.js.map +0 -1
- package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +0 -1
- package/gui/web/dist/assets/index-DBrWq43L.css +0 -1
- package/gui/web/dist/assets/index-RflHaj0y.js +0 -67
- package/src/runtime/index.ts +0 -55
- package/src/runtime/provider-ids.ts +0 -15
- package/src/workflows/types.ts +0 -4
|
@@ -1,12 +1,23 @@
|
|
|
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
1
|
import { StealthBrowser } from "../infra/browser.js";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
2
|
+
import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
|
|
3
|
+
import { HttpError, httpGet } from "../infra/http-client.js";
|
|
4
|
+
import { rateLimiter } from "../infra/rate-limiter.js";
|
|
7
5
|
import { computeGreeks } from "../tools/options/greeks.js";
|
|
6
|
+
import type { OHLCV, StockQuote } from "../types/market.js";
|
|
7
|
+
import type {
|
|
8
|
+
OptionContract,
|
|
9
|
+
OptionsChain,
|
|
10
|
+
OptionsMarketSession,
|
|
11
|
+
OptionsQuoteStatus,
|
|
12
|
+
} from "../types/options.js";
|
|
13
|
+
import type { FundHoldings } from "../types/portfolio.js";
|
|
14
|
+
import { InvalidSymbolError } from "./errors.js";
|
|
8
15
|
|
|
9
16
|
const BASE_URL = "https://query1.finance.yahoo.com/v8/finance/chart";
|
|
17
|
+
const QUOTE_SUMMARY_URL = "https://query1.finance.yahoo.com/v10/finance/quoteSummary";
|
|
18
|
+
const STALE_QUOTE_MAX_RETRY_AFTER_MS = 1_000;
|
|
19
|
+
|
|
20
|
+
type YahooNumber = number | { raw?: number; fmt?: string };
|
|
10
21
|
|
|
11
22
|
interface YahooChartResponse {
|
|
12
23
|
chart: {
|
|
@@ -28,6 +39,29 @@ interface YahooChartResponse {
|
|
|
28
39
|
};
|
|
29
40
|
}
|
|
30
41
|
|
|
42
|
+
interface YahooQuoteSummaryResponse {
|
|
43
|
+
quoteSummary: {
|
|
44
|
+
result?: Array<{
|
|
45
|
+
price?: {
|
|
46
|
+
symbol?: string;
|
|
47
|
+
shortName?: string;
|
|
48
|
+
longName?: string;
|
|
49
|
+
};
|
|
50
|
+
topHoldings?: {
|
|
51
|
+
holdings?: Array<{
|
|
52
|
+
symbol?: string;
|
|
53
|
+
holdingName?: string;
|
|
54
|
+
holdingPercent?: YahooNumber;
|
|
55
|
+
}>;
|
|
56
|
+
equityHoldings?: {
|
|
57
|
+
sectorWeightings?: Array<Record<string, YahooNumber>>;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}>;
|
|
61
|
+
error?: { code?: string; description?: string } | null;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
export async function getQuote(symbol: string): Promise<StockQuote> {
|
|
32
66
|
const cacheKey = `yahoo:quote:${symbol}`;
|
|
33
67
|
const cached = cache.get<StockQuote>(cacheKey);
|
|
@@ -39,6 +73,7 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
|
|
|
39
73
|
const url = `${BASE_URL}/${encodeURIComponent(symbol)}?interval=1d&range=1d`;
|
|
40
74
|
const data = await httpGet<YahooChartResponse>(url, {
|
|
41
75
|
headers: { "User-Agent": "OpenCandle/1.0" },
|
|
76
|
+
maxRetryAfterMs: STALE_QUOTE_MAX_RETRY_AFTER_MS,
|
|
42
77
|
});
|
|
43
78
|
|
|
44
79
|
if (data.chart.error) {
|
|
@@ -72,8 +107,16 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
|
|
|
72
107
|
week52High: meta.fiftyTwoWeekHigh ?? 0,
|
|
73
108
|
week52Low: meta.fiftyTwoWeekLow ?? 0,
|
|
74
109
|
timestamp: Date.now(),
|
|
110
|
+
currency:
|
|
111
|
+
typeof meta.currency === "string" && meta.currency.trim() !== ""
|
|
112
|
+
? meta.currency.trim().toUpperCase()
|
|
113
|
+
: null,
|
|
75
114
|
};
|
|
76
115
|
|
|
116
|
+
if (isZeroResultQuote(quote)) {
|
|
117
|
+
throw new InvalidSymbolError(symbol.toUpperCase(), "yahoo");
|
|
118
|
+
}
|
|
119
|
+
|
|
77
120
|
cache.set(cacheKey, quote, TTL.QUOTE);
|
|
78
121
|
return quote;
|
|
79
122
|
} catch (error) {
|
|
@@ -83,6 +126,16 @@ export async function getQuote(symbol: string): Promise<StockQuote> {
|
|
|
83
126
|
}
|
|
84
127
|
}
|
|
85
128
|
|
|
129
|
+
function isZeroResultQuote(quote: StockQuote): boolean {
|
|
130
|
+
return (
|
|
131
|
+
quote.price === 0 &&
|
|
132
|
+
quote.volume === 0 &&
|
|
133
|
+
quote.week52High === 0 &&
|
|
134
|
+
quote.week52Low === 0 &&
|
|
135
|
+
quote.marketCap === 0
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
86
139
|
export async function getHistory(
|
|
87
140
|
symbol: string,
|
|
88
141
|
range: string = "6mo",
|
|
@@ -128,10 +181,128 @@ export async function getHistory(
|
|
|
128
181
|
}
|
|
129
182
|
}
|
|
130
183
|
|
|
184
|
+
export async function getFundHoldings(symbol: string): Promise<FundHoldings> {
|
|
185
|
+
const normalizedSymbol = symbol.toUpperCase();
|
|
186
|
+
const cacheKey = `yahoo:fund-holdings:${normalizedSymbol}`;
|
|
187
|
+
const cached = cache.get<FundHoldings>(cacheKey);
|
|
188
|
+
if (cached) return cached;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await rateLimiter.acquire("yahoo");
|
|
192
|
+
|
|
193
|
+
const data = await getFundHoldingsSummary(normalizedSymbol);
|
|
194
|
+
const result = data.quoteSummary.result?.[0];
|
|
195
|
+
if (data.quoteSummary.error) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Yahoo Finance: ${data.quoteSummary.error.description ?? data.quoteSummary.error.code ?? "quoteSummary error"}`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
if (!result?.topHoldings?.holdings?.length) {
|
|
201
|
+
throw new Error(`Yahoo Finance: no fund holdings returned for ${normalizedSymbol}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const holdings: FundHoldings = {
|
|
205
|
+
symbol: result.price?.symbol?.toUpperCase() ?? normalizedSymbol,
|
|
206
|
+
name: result.price?.shortName ?? result.price?.longName,
|
|
207
|
+
provider: "yahoo",
|
|
208
|
+
holdings: result.topHoldings.holdings.flatMap((holding) => {
|
|
209
|
+
const holdingSymbol = holding.symbol?.trim().toUpperCase();
|
|
210
|
+
const weight = normalizeHoldingWeight(holding.holdingPercent);
|
|
211
|
+
if (!holdingSymbol || weight === undefined) return [];
|
|
212
|
+
return [
|
|
213
|
+
{
|
|
214
|
+
symbol: holdingSymbol,
|
|
215
|
+
name: holding.holdingName?.trim() || holdingSymbol,
|
|
216
|
+
weight,
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
}),
|
|
220
|
+
sectorWeights: normalizeSectorWeights(result.topHoldings.equityHoldings?.sectorWeightings),
|
|
221
|
+
};
|
|
222
|
+
if (holdings.holdings.length === 0) {
|
|
223
|
+
throw new Error(`Yahoo Finance: no weighted fund holdings returned for ${normalizedSymbol}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
cache.set(cacheKey, holdings, TTL.FUNDAMENTALS);
|
|
227
|
+
return holdings;
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const stale = cache.getStale<FundHoldings>(cacheKey, STALE_LIMIT.FUNDAMENTALS);
|
|
230
|
+
if (stale) return stale.value;
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function getFundHoldingsSummary(symbol: string): Promise<YahooQuoteSummaryResponse> {
|
|
236
|
+
try {
|
|
237
|
+
return await fetchFundHoldingsSummary(symbol);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (!isYahooAuthError(error)) throw error;
|
|
240
|
+
return fetchFundHoldingsSummaryWithCrumb(symbol);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function fetchFundHoldingsSummary(symbol: string): Promise<YahooQuoteSummaryResponse> {
|
|
245
|
+
const modules = encodeURIComponent("price,topHoldings");
|
|
246
|
+
const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}`;
|
|
247
|
+
return httpGet<YahooQuoteSummaryResponse>(url, {
|
|
248
|
+
headers: { "User-Agent": "OpenCandle/1.0" },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function fetchFundHoldingsSummaryWithCrumb(
|
|
253
|
+
symbol: string,
|
|
254
|
+
): Promise<YahooQuoteSummaryResponse> {
|
|
255
|
+
const modules = encodeURIComponent("price,topHoldings");
|
|
256
|
+
const { crumb, cookie } = await getYahooCrumb();
|
|
257
|
+
const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}&crumb=${encodeURIComponent(crumb)}`;
|
|
258
|
+
try {
|
|
259
|
+
return await httpGet<YahooQuoteSummaryResponse>(url, {
|
|
260
|
+
headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
|
|
261
|
+
});
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (!isYahooAuthError(error)) throw error;
|
|
264
|
+
clearCrumbCache();
|
|
265
|
+
const fresh = await getYahooCrumb();
|
|
266
|
+
const retryUrl = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(symbol)}?modules=${modules}&crumb=${encodeURIComponent(fresh.crumb)}`;
|
|
267
|
+
return httpGet<YahooQuoteSummaryResponse>(retryUrl, {
|
|
268
|
+
headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function isYahooAuthError(error: unknown): boolean {
|
|
274
|
+
return error instanceof HttpError && (error.status === 401 || error.status === 429);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeHoldingWeight(value: YahooNumber | undefined): number | undefined {
|
|
278
|
+
const numeric = typeof value === "number" ? value : value?.raw;
|
|
279
|
+
if (numeric === undefined || !Number.isFinite(numeric) || numeric <= 0) return undefined;
|
|
280
|
+
return numeric > 1 ? roundWeight(numeric / 100) : roundWeight(numeric);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function normalizeSectorWeights(
|
|
284
|
+
sectors: Array<Record<string, YahooNumber>> | undefined,
|
|
285
|
+
): Record<string, number> | undefined {
|
|
286
|
+
if (!sectors?.length) return undefined;
|
|
287
|
+
const weights: Record<string, number> = {};
|
|
288
|
+
for (const sector of sectors) {
|
|
289
|
+
for (const [name, rawWeight] of Object.entries(sector)) {
|
|
290
|
+
const weight = normalizeHoldingWeight(rawWeight);
|
|
291
|
+
if (weight !== undefined) weights[name] = weight;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return Object.keys(weights).length > 0 ? weights : undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function roundWeight(value: number): number {
|
|
298
|
+
return Math.round(value * 10_000) / 10_000;
|
|
299
|
+
}
|
|
300
|
+
|
|
131
301
|
// --- Options Chain (v7 API with crumb+cookie auth) ---
|
|
132
302
|
|
|
133
303
|
const BROWSER_UA =
|
|
134
304
|
"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";
|
|
305
|
+
const YAHOO_RAW_FETCH_TIMEOUT_MS = 10_000;
|
|
135
306
|
|
|
136
307
|
let cachedCrumb: { crumb: string; cookie: string; expiresAt: number } | null = null;
|
|
137
308
|
|
|
@@ -147,15 +318,30 @@ export async function getYahooCrumb(): Promise<{ crumb: string; cookie: string }
|
|
|
147
318
|
// Step 1: Hit fc.yahoo.com to get a session cookie
|
|
148
319
|
const cookieRes = await fetch("https://fc.yahoo.com/t", {
|
|
149
320
|
headers: { "User-Agent": BROWSER_UA },
|
|
321
|
+
signal: yahooRawFetchSignal(),
|
|
150
322
|
});
|
|
151
323
|
const setCookie = cookieRes.headers.get("set-cookie") ?? "";
|
|
152
324
|
const cookie = setCookie.split(";")[0]; // Extract just the cookie value
|
|
325
|
+
if (!cookie) {
|
|
326
|
+
if (!cookieRes.ok) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`Yahoo crumb cookie request failed: HTTP ${cookieRes.status} ${cookieRes.statusText}`.trim(),
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
throw new Error("Yahoo crumb cookie request did not return a session cookie");
|
|
332
|
+
}
|
|
153
333
|
|
|
154
334
|
// Step 2: Use the cookie to get a crumb
|
|
155
335
|
const crumbRes = await fetch("https://query2.finance.yahoo.com/v1/test/getcrumb", {
|
|
156
336
|
headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
|
|
337
|
+
signal: yahooRawFetchSignal(),
|
|
157
338
|
});
|
|
158
|
-
|
|
339
|
+
if (!crumbRes.ok) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
`Yahoo crumb request failed: HTTP ${crumbRes.status} ${crumbRes.statusText}`.trim(),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const crumb = (await crumbRes.text()).trim();
|
|
159
345
|
|
|
160
346
|
if (!crumb || crumb.includes("Unauthorized")) {
|
|
161
347
|
throw new Error("Failed to acquire Yahoo Finance crumb");
|
|
@@ -182,25 +368,23 @@ interface YahooOptionsResponse {
|
|
|
182
368
|
};
|
|
183
369
|
}
|
|
184
370
|
|
|
185
|
-
export async function getOptionsChain(
|
|
186
|
-
symbol: string,
|
|
187
|
-
expiration?: number,
|
|
188
|
-
): Promise<OptionsChain> {
|
|
371
|
+
export async function getOptionsChain(symbol: string, expiration?: number): Promise<OptionsChain> {
|
|
189
372
|
const cacheKey = `yahoo:options:${symbol}:${expiration ?? "nearest"}`;
|
|
190
373
|
const cached = cache.get<OptionsChain>(cacheKey);
|
|
191
374
|
if (cached) return cached;
|
|
192
375
|
|
|
193
376
|
await rateLimiter.acquire("yahoo");
|
|
194
377
|
|
|
195
|
-
const { crumb, cookie } = await getYahooCrumb();
|
|
196
378
|
const dateParam = expiration ? `&date=${expiration}` : "";
|
|
197
|
-
const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
|
|
198
379
|
|
|
199
380
|
let res: Response | null = null;
|
|
200
381
|
let fetchError: unknown;
|
|
201
382
|
try {
|
|
383
|
+
const { crumb, cookie } = await getYahooCrumb();
|
|
384
|
+
const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
|
|
202
385
|
res = await fetch(url, {
|
|
203
386
|
headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
|
|
387
|
+
signal: yahooRawFetchSignal(),
|
|
204
388
|
});
|
|
205
389
|
} catch (error) {
|
|
206
390
|
fetchError = error;
|
|
@@ -214,6 +398,7 @@ export async function getOptionsChain(
|
|
|
214
398
|
const retryUrl = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(fresh.crumb)}${dateParam}`;
|
|
215
399
|
res = await fetch(retryUrl, {
|
|
216
400
|
headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
|
|
401
|
+
signal: yahooRawFetchSignal(),
|
|
217
402
|
});
|
|
218
403
|
} catch (error) {
|
|
219
404
|
fetchError = error;
|
|
@@ -227,7 +412,7 @@ export async function getOptionsChain(
|
|
|
227
412
|
try {
|
|
228
413
|
const browserData = await fetchOptionsViaBrowser(symbol, expiration);
|
|
229
414
|
if (browserData) {
|
|
230
|
-
const chain = parseOptionsResponse(
|
|
415
|
+
const chain = parseOptionsResponse(browserData);
|
|
231
416
|
cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
|
|
232
417
|
return chain;
|
|
233
418
|
}
|
|
@@ -245,18 +430,25 @@ export async function getOptionsChain(
|
|
|
245
430
|
throw new Error(message);
|
|
246
431
|
}
|
|
247
432
|
if (browserError instanceof Error) {
|
|
248
|
-
const message =
|
|
433
|
+
const message =
|
|
434
|
+
fetchError instanceof Error ? fetchError.message : "Yahoo Finance options: fetch failed";
|
|
249
435
|
throw new Error(`${message}; browser fallback failed: ${browserError.message}`);
|
|
250
436
|
}
|
|
251
|
-
throw fetchError instanceof Error
|
|
437
|
+
throw fetchError instanceof Error
|
|
438
|
+
? fetchError
|
|
439
|
+
: new Error("Yahoo Finance options: fetch failed");
|
|
252
440
|
}
|
|
253
441
|
|
|
254
442
|
const data: YahooOptionsResponse = await res.json();
|
|
255
|
-
const chain = parseOptionsResponse(
|
|
443
|
+
const chain = parseOptionsResponse(data);
|
|
256
444
|
cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
|
|
257
445
|
return chain;
|
|
258
446
|
}
|
|
259
447
|
|
|
448
|
+
function yahooRawFetchSignal(): AbortSignal {
|
|
449
|
+
return AbortSignal.timeout(YAHOO_RAW_FETCH_TIMEOUT_MS);
|
|
450
|
+
}
|
|
451
|
+
|
|
260
452
|
/**
|
|
261
453
|
* Compute time to expiry in years from a Yahoo expiration timestamp (midnight UTC).
|
|
262
454
|
* US equity options expire at 4:00 PM ET. During EDT that is 20:00 UTC.
|
|
@@ -265,7 +457,7 @@ export async function getOptionsChain(
|
|
|
265
457
|
*/
|
|
266
458
|
export function computeTimeToExpiry(expirationTs: number, nowMs: number = Date.now()): number {
|
|
267
459
|
const MARKET_CLOSE_OFFSET_S = 21 * 3600; // 21:00 UTC ≈ 4 PM ET
|
|
268
|
-
const MIN_TIME_YEARS = 1 / (365 * 24);
|
|
460
|
+
const MIN_TIME_YEARS = 1 / (365 * 24); // ~1 hour floor
|
|
269
461
|
const SECONDS_PER_YEAR = 365 * 24 * 3600;
|
|
270
462
|
|
|
271
463
|
const expiryCloseTs = expirationTs + MARKET_CLOSE_OFFSET_S;
|
|
@@ -275,7 +467,73 @@ export function computeTimeToExpiry(expirationTs: number, nowMs: number = Date.n
|
|
|
275
467
|
return Math.max(MIN_TIME_YEARS, remainingS / SECONDS_PER_YEAR);
|
|
276
468
|
}
|
|
277
469
|
|
|
278
|
-
function
|
|
470
|
+
function getUsOptionsMarketSession(now: Date = new Date()): OptionsMarketSession {
|
|
471
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
472
|
+
timeZone: "America/New_York",
|
|
473
|
+
weekday: "short",
|
|
474
|
+
hour: "2-digit",
|
|
475
|
+
minute: "2-digit",
|
|
476
|
+
hour12: false,
|
|
477
|
+
}).formatToParts(now);
|
|
478
|
+
const part = (type: string): string => parts.find((p) => p.type === type)?.value ?? "";
|
|
479
|
+
const weekday = part("weekday");
|
|
480
|
+
if (weekday === "Sat" || weekday === "Sun") return "closed";
|
|
481
|
+
|
|
482
|
+
const hour = Number(part("hour"));
|
|
483
|
+
const minute = Number(part("minute"));
|
|
484
|
+
const minutes = hour * 60 + minute;
|
|
485
|
+
if (minutes < 9 * 60 + 30) return "pre_market";
|
|
486
|
+
if (minutes < 16 * 60) return "regular";
|
|
487
|
+
return "after_hours";
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function buildOptionsQuoteStatus(
|
|
491
|
+
contracts: OptionContract[],
|
|
492
|
+
now: Date = new Date(),
|
|
493
|
+
): OptionsQuoteStatus {
|
|
494
|
+
const marketSession = getUsOptionsMarketSession(now);
|
|
495
|
+
const totalContracts = contracts.length;
|
|
496
|
+
const zeroBidAskContracts = contracts.filter((c) => c.bid === 0 && c.ask === 0).length;
|
|
497
|
+
const allZeroBidAsk = totalContracts > 0 && zeroBidAskContracts === totalContracts;
|
|
498
|
+
const hasLiveBidAsk = contracts.some((c) => c.bid > 0 || c.ask > 0);
|
|
499
|
+
|
|
500
|
+
if (allZeroBidAsk && marketSession !== "regular") {
|
|
501
|
+
return {
|
|
502
|
+
marketSession,
|
|
503
|
+
bidAskState: "closed_market_or_stale_quotes",
|
|
504
|
+
zeroBidAskContracts,
|
|
505
|
+
totalContracts,
|
|
506
|
+
warning:
|
|
507
|
+
"All option contracts have $0.00/$0.00 bid/ask before regular options trading or outside market hours; treat bid/ask as closed-market or stale until the market opens.",
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (allZeroBidAsk) {
|
|
512
|
+
return {
|
|
513
|
+
marketSession,
|
|
514
|
+
bidAskState: "live_zero_bid_ask",
|
|
515
|
+
zeroBidAskContracts,
|
|
516
|
+
totalContracts,
|
|
517
|
+
warning:
|
|
518
|
+
"All option contracts have $0.00/$0.00 bid/ask during regular options trading hours; verify with a broker, but this may indicate live illiquidity.",
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
marketSession,
|
|
524
|
+
bidAskState: hasLiveBidAsk ? "live_quotes" : "mixed_or_unknown",
|
|
525
|
+
zeroBidAskContracts,
|
|
526
|
+
totalContracts,
|
|
527
|
+
...(marketSession !== "regular"
|
|
528
|
+
? {
|
|
529
|
+
warning:
|
|
530
|
+
"Options bid/ask quotes may be stale outside regular options trading hours; verify live executable prices after the market opens.",
|
|
531
|
+
}
|
|
532
|
+
: {}),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function parseOptionsResponse(data: YahooOptionsResponse): OptionsChain {
|
|
279
537
|
if (data.optionChain.error) {
|
|
280
538
|
throw new Error(`Yahoo Finance options: ${JSON.stringify(data.optionChain.error)}`);
|
|
281
539
|
}
|
|
@@ -284,6 +542,9 @@ function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): Optio
|
|
|
284
542
|
const quote = result.quote;
|
|
285
543
|
const underlyingPrice = quote.regularMarketPrice ?? 0;
|
|
286
544
|
const opts = result.options[0];
|
|
545
|
+
if (!opts && underlyingPrice === 0) {
|
|
546
|
+
throw new InvalidSymbolError(result.underlyingSymbol, "yahoo");
|
|
547
|
+
}
|
|
287
548
|
const riskFreeRate = 0.05;
|
|
288
549
|
|
|
289
550
|
const expirationTs = opts.expirationDate;
|
|
@@ -293,7 +554,14 @@ function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): Optio
|
|
|
293
554
|
const mapContract = (c: any, type: "call" | "put"): OptionContract => {
|
|
294
555
|
const strike = c.strike ?? c.strike?.raw ?? 0;
|
|
295
556
|
const iv = c.impliedVolatility ?? c.impliedVolatility?.raw ?? 0;
|
|
296
|
-
const greeks = computeGreeks({
|
|
557
|
+
const greeks = computeGreeks({
|
|
558
|
+
type,
|
|
559
|
+
spot: underlyingPrice,
|
|
560
|
+
strike,
|
|
561
|
+
timeYears,
|
|
562
|
+
iv,
|
|
563
|
+
riskFreeRate,
|
|
564
|
+
});
|
|
297
565
|
return {
|
|
298
566
|
contractSymbol: c.contractSymbol ?? "",
|
|
299
567
|
type,
|
|
@@ -314,17 +582,21 @@ function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): Optio
|
|
|
314
582
|
const puts = (opts.puts ?? []).map((c: any) => mapContract(c, "put"));
|
|
315
583
|
const totalCallVolume = calls.reduce((s, c) => s + c.volume, 0);
|
|
316
584
|
const totalPutVolume = puts.reduce((s, c) => s + c.volume, 0);
|
|
585
|
+
const quoteStatus = buildOptionsQuoteStatus([...calls, ...puts]);
|
|
317
586
|
|
|
318
587
|
return {
|
|
319
588
|
symbol: result.underlyingSymbol,
|
|
320
589
|
underlyingPrice,
|
|
321
590
|
expirationDate,
|
|
322
|
-
expirationDates: result.expirationDates.map(
|
|
591
|
+
expirationDates: result.expirationDates.map(
|
|
592
|
+
(ts) => new Date(ts * 1000).toISOString().split("T")[0],
|
|
593
|
+
),
|
|
323
594
|
calls,
|
|
324
595
|
puts,
|
|
325
596
|
totalCallVolume,
|
|
326
597
|
totalPutVolume,
|
|
327
598
|
putCallRatio: totalCallVolume > 0 ? totalPutVolume / totalCallVolume : 0,
|
|
599
|
+
quoteStatus,
|
|
328
600
|
fetchedAt: new Date().toISOString(),
|
|
329
601
|
};
|
|
330
602
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ClassificationResult, WorkflowType, ExtractedEntities } from "./types.js";
|
|
2
1
|
import { extractEntities } from "./entity-extractor.js";
|
|
2
|
+
import type { ClassificationResult, ExtractedEntities, WorkflowType } from "./types.js";
|
|
3
3
|
|
|
4
4
|
interface Rule {
|
|
5
5
|
workflow: WorkflowType;
|
|
@@ -29,7 +29,23 @@ const RULES: Rule[] = [
|
|
|
29
29
|
entities.symbols.length === 1 &&
|
|
30
30
|
(/\bis\s+\S+\s+(?:attractive|undervalued|overvalued|cheap|expensive)/i.test(lower) ||
|
|
31
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)
|
|
32
|
+
/\bwhat\s+do\s+you\s+think\s+(?:of|about)\s+\$?[a-z]{1,5}\b/i.test(lower) ||
|
|
33
|
+
/\bbull\s+(?:and|or)\s+bear\s+case\b/i.test(lower))
|
|
34
|
+
);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
// Portfolio risk for existing holdings must route before multi-symbol compare.
|
|
38
|
+
{
|
|
39
|
+
workflow: "watchlist_or_tracking",
|
|
40
|
+
confidence: 0.9,
|
|
41
|
+
test: (input, entities) => {
|
|
42
|
+
const lower = input.toLowerCase();
|
|
43
|
+
return (
|
|
44
|
+
entities.symbols.length >= 1 &&
|
|
45
|
+
(/\bi\s+own\b/.test(lower) || /\bmy\s+holdings\b/.test(lower)) &&
|
|
46
|
+
(/\bportfolio\s+risk\b/.test(lower) ||
|
|
47
|
+
/\bbiggest\s+risk\b/.test(lower) ||
|
|
48
|
+
/\bconcentration\b/.test(lower))
|
|
33
49
|
);
|
|
34
50
|
},
|
|
35
51
|
},
|
|
@@ -56,6 +72,65 @@ const RULES: Rule[] = [
|
|
|
56
72
|
return hasNewsKeyword;
|
|
57
73
|
},
|
|
58
74
|
},
|
|
75
|
+
// Tool-backed finance tasks that are not a structured multi-step workflow.
|
|
76
|
+
{
|
|
77
|
+
workflow: "general_finance_qa",
|
|
78
|
+
confidence: 0.9,
|
|
79
|
+
test: (input, entities) => {
|
|
80
|
+
const lower = input.toLowerCase();
|
|
81
|
+
const hasOptionKeywords =
|
|
82
|
+
/\bcalls?\b/.test(lower) ||
|
|
83
|
+
/\bputs?\b/.test(lower) ||
|
|
84
|
+
/\boption(?:s)?\s*chain\b/.test(lower) ||
|
|
85
|
+
/\boptions?\b/.test(lower);
|
|
86
|
+
const hasCompareKeywords =
|
|
87
|
+
/\bcompare\b/.test(lower) ||
|
|
88
|
+
/\bvs\.?\b/.test(lower) ||
|
|
89
|
+
/\bversus\b/.test(lower) ||
|
|
90
|
+
/\bwhich\s+is\s+better\b/.test(lower);
|
|
91
|
+
|
|
92
|
+
if (hasOptionKeywords && entities.symbols.length >= 1) return false;
|
|
93
|
+
if (hasCompareKeywords && entities.symbols.length >= 2) return false;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
/\bbacktest\b/.test(lower) || /\bsentiment\b/.test(lower) || /\brate\s+cuts?\b/.test(lower)
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
// Broad market / sector / macro research that should receive the general
|
|
101
|
+
// analyst fallback rather than disappearing into an unclassified turn.
|
|
102
|
+
{
|
|
103
|
+
workflow: "general_finance_qa",
|
|
104
|
+
confidence: 0.85,
|
|
105
|
+
test: (input) => {
|
|
106
|
+
const lower = input.toLowerCase();
|
|
107
|
+
const hasResearchVerb =
|
|
108
|
+
/\banaly[sz]e\b/.test(lower) ||
|
|
109
|
+
/\bevaluat(?:e|ion)\b/.test(lower) ||
|
|
110
|
+
/\breview\b/.test(lower) ||
|
|
111
|
+
/\bdiscuss\b/.test(lower) ||
|
|
112
|
+
/\bpredict\b/.test(lower) ||
|
|
113
|
+
/\bassess\b/.test(lower) ||
|
|
114
|
+
/^what\b/.test(lower);
|
|
115
|
+
const hasBroadFinanceTopic =
|
|
116
|
+
/\bmarket\s+structure\b/.test(lower) ||
|
|
117
|
+
/\b(?:sector|industry)\b/.test(lower) ||
|
|
118
|
+
/\bmacro\s+risks?\b/.test(lower) ||
|
|
119
|
+
/\bmonetary\s+policy\b/.test(lower) ||
|
|
120
|
+
/\bemerging\s+markets?\b/.test(lower) ||
|
|
121
|
+
/\bcapital\s+flows?\b/.test(lower) ||
|
|
122
|
+
/\bcurrency\s+fluctuations?\b/.test(lower) ||
|
|
123
|
+
/\binflation\b/.test(lower);
|
|
124
|
+
return hasResearchVerb && hasBroadFinanceTopic;
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
// Existing allocation / portfolio review. This is not portfolio construction
|
|
128
|
+
// and should not require a budget.
|
|
129
|
+
{
|
|
130
|
+
workflow: "general_finance_qa",
|
|
131
|
+
confidence: 0.85,
|
|
132
|
+
test: (input) => isPortfolioEvaluationRequest(input),
|
|
133
|
+
},
|
|
59
134
|
// Options: symbol + option keyword
|
|
60
135
|
{
|
|
61
136
|
workflow: "options_screener",
|
|
@@ -70,6 +145,14 @@ const RULES: Rule[] = [
|
|
|
70
145
|
return hasOptionKeywords && entities.symbols.length >= 1;
|
|
71
146
|
},
|
|
72
147
|
},
|
|
148
|
+
// Stateful portfolio/watchlist/alert/prediction mutations must not be
|
|
149
|
+
// mistaken for compare or portfolio-construction workflows just because a
|
|
150
|
+
// cost basis, target, or currency token is present.
|
|
151
|
+
{
|
|
152
|
+
workflow: "watchlist_or_tracking",
|
|
153
|
+
confidence: 0.95,
|
|
154
|
+
test: (input) => isStatefulTrackingRequest(input),
|
|
155
|
+
},
|
|
73
156
|
// Compare: keyword + 2+ symbols (uppercase)
|
|
74
157
|
{
|
|
75
158
|
workflow: "compare_assets",
|
|
@@ -88,9 +171,12 @@ const RULES: Rule[] = [
|
|
|
88
171
|
{
|
|
89
172
|
workflow: "compare_assets",
|
|
90
173
|
confidence: 0.85,
|
|
91
|
-
test: (input) => {
|
|
174
|
+
test: (input, entities) => {
|
|
92
175
|
const lower = input.toLowerCase();
|
|
93
|
-
return
|
|
176
|
+
return (
|
|
177
|
+
entities.symbols.length >= 2 &&
|
|
178
|
+
/\bcompare\s+[a-z]{1,5}\b(?:\s*,?\s*(?:and\s+)?[a-z]{1,5}\b)+/.test(lower)
|
|
179
|
+
);
|
|
94
180
|
},
|
|
95
181
|
},
|
|
96
182
|
// Compare: 2+ uppercase symbols without explicit keyword
|
|
@@ -161,6 +247,9 @@ const RULES: Rule[] = [
|
|
|
161
247
|
},
|
|
162
248
|
];
|
|
163
249
|
|
|
250
|
+
/**
|
|
251
|
+
* @deprecated Use the LLM router (`route`) for new classification paths; keep this only for rules-mode fallback and deterministic safety nets.
|
|
252
|
+
*/
|
|
164
253
|
export function classifyIntent(input: string): ClassificationResult {
|
|
165
254
|
const trimmed = input.trim();
|
|
166
255
|
if (!trimmed) {
|
|
@@ -192,3 +281,96 @@ export function classifyIntent(input: string): ClassificationResult {
|
|
|
192
281
|
entities,
|
|
193
282
|
};
|
|
194
283
|
}
|
|
284
|
+
|
|
285
|
+
function isPortfolioEvaluationRequest(input: string): boolean {
|
|
286
|
+
const lower = input.toLowerCase();
|
|
287
|
+
const hasEvaluationIntent =
|
|
288
|
+
/\b(?:evaluat(?:e|ion)|review|assess|analy[sz]e|prospects?|risks?|opportunities?|mitigat(?:e|ion)|adjustment)\b/.test(
|
|
289
|
+
lower,
|
|
290
|
+
);
|
|
291
|
+
const hasPortfolioObject =
|
|
292
|
+
/\b(?:portfolio|allocation|asset\s+allocation|60\/40|equity|fixed\s+income|bonds?)\b/.test(
|
|
293
|
+
lower,
|
|
294
|
+
);
|
|
295
|
+
const hasConstructionIntent =
|
|
296
|
+
/\b(?:build|create|construct|put\s+together|invest|allocate)\b/.test(lower) &&
|
|
297
|
+
/\$\s*\d|\b\d+(?:\.\d+)?\s*k\b|\bbudget\b|\bcapital\b/.test(lower);
|
|
298
|
+
return hasEvaluationIntent && hasPortfolioObject && !hasConstructionIntent;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function isStatefulTrackingRequest(input: string): boolean {
|
|
302
|
+
const lower = input.toLowerCase();
|
|
303
|
+
const hasPortfolioConstructionIntent =
|
|
304
|
+
/\b(?:build|create|construct|put\s+together)\b/.test(lower) &&
|
|
305
|
+
/\bportfolio\b/.test(lower) &&
|
|
306
|
+
/\$\s*\d|\b\d+(?:\.\d+)?\s*k\b|\bbudget\b|\bcapital\b/.test(lower);
|
|
307
|
+
const hasStateVerb =
|
|
308
|
+
/\b(?:add|remove|update|record|track|create|configure|check|show|list|view|cancel)\b/.test(
|
|
309
|
+
lower,
|
|
310
|
+
);
|
|
311
|
+
const hasStateObject =
|
|
312
|
+
/\b(?:watchlist|portfolio|holding|holdings|position|positions|prediction|predictions|alert|alerts|daily\s+report|watchlist\s+report|report\s+history)\b/.test(
|
|
313
|
+
lower,
|
|
314
|
+
);
|
|
315
|
+
const hasPortfolioLotShape =
|
|
316
|
+
/\b(?:add|record|track)\b/.test(lower) &&
|
|
317
|
+
/\b\d+(?:\.\d+)?\s+shares?\b/.test(lower) &&
|
|
318
|
+
/\b(?:portfolio|holding|holdings|position|positions)\b/.test(lower);
|
|
319
|
+
if (hasPortfolioConstructionIntent) return false;
|
|
320
|
+
return (hasStateVerb && hasStateObject) || hasPortfolioLotShape;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const FINANCE_SIGNAL_TERMS = [
|
|
324
|
+
"stock",
|
|
325
|
+
"stocks",
|
|
326
|
+
"shares",
|
|
327
|
+
"ticker",
|
|
328
|
+
"tickers",
|
|
329
|
+
"etf",
|
|
330
|
+
"etfs",
|
|
331
|
+
"ipo",
|
|
332
|
+
"earnings",
|
|
333
|
+
"dividend",
|
|
334
|
+
"dividends",
|
|
335
|
+
"valuation",
|
|
336
|
+
"stock market",
|
|
337
|
+
"invest",
|
|
338
|
+
"investing",
|
|
339
|
+
"investment",
|
|
340
|
+
"portfolio",
|
|
341
|
+
"watchlist",
|
|
342
|
+
"bond",
|
|
343
|
+
"bonds",
|
|
344
|
+
"bond yield",
|
|
345
|
+
"treasury",
|
|
346
|
+
"the fed",
|
|
347
|
+
"inflation",
|
|
348
|
+
"interest rates",
|
|
349
|
+
"crypto",
|
|
350
|
+
"bitcoin",
|
|
351
|
+
"ethereum",
|
|
352
|
+
"options chain",
|
|
353
|
+
"covered call",
|
|
354
|
+
"puts",
|
|
355
|
+
"bullish",
|
|
356
|
+
"bearish",
|
|
357
|
+
"hedge",
|
|
358
|
+
"price target",
|
|
359
|
+
"cost basis",
|
|
360
|
+
"nasdaq",
|
|
361
|
+
"s&p",
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
const FINANCE_SIGNAL_PATTERN = new RegExp(
|
|
365
|
+
`\\b(?:${FINANCE_SIGNAL_TERMS.map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`,
|
|
366
|
+
"i",
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Deterministic finance-vocabulary check for rules-mode fallback turns whose
|
|
371
|
+
* intent did not match a workflow and whose entities carry no symbols (for
|
|
372
|
+
* example theme prompts about private companies or sectors).
|
|
373
|
+
*/
|
|
374
|
+
export function hasFinanceSignals(input: string): boolean {
|
|
375
|
+
return FINANCE_SIGNAL_PATTERN.test(input);
|
|
376
|
+
}
|