opencandle 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +106 -14
- package/assets/logo.svg +187 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +11 -3
- package/dist/infra/browser.js +2 -1
- package/dist/infra/browser.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +1 -0
- package/dist/infra/native-dependencies.js +10 -0
- package/dist/infra/native-dependencies.js.map +1 -0
- package/dist/infra/node-version.d.ts +2 -0
- package/dist/infra/node-version.js +23 -0
- package/dist/infra/node-version.js.map +1 -0
- package/dist/infra/rate-limiter.d.ts +4 -0
- package/dist/infra/rate-limiter.js +5 -1
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +28 -11
- package/dist/memory/manager.js.map +1 -1
- package/dist/memory/sqlite.js +42 -4
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.d.ts +7 -0
- package/dist/memory/storage.js +3 -3
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.d.ts +8 -0
- package/dist/memory/tool-defaults.js +59 -0
- package/dist/memory/tool-defaults.js.map +1 -0
- package/dist/memory/types.js +4 -0
- package/dist/memory/types.js.map +1 -1
- package/dist/onboarding/connect.d.ts +13 -1
- package/dist/onboarding/connect.js +21 -10
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/prompt-user.d.ts +1 -1
- package/dist/onboarding/providers.d.ts +7 -0
- package/dist/onboarding/providers.js +6 -3
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +1 -1
- package/dist/pi/opencandle-extension.d.ts +7 -1
- package/dist/pi/opencandle-extension.js +391 -21
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-storage.d.ts +2 -0
- package/dist/pi/session-storage.js +5 -0
- package/dist/pi/session-storage.js.map +1 -0
- package/dist/pi/session.d.ts +4 -1
- package/dist/pi/session.js +25 -3
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.d.ts +1 -1
- package/dist/pi/setup.js +11 -1
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +2 -2
- package/dist/pi/tool-adapter.js +14 -1
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +40 -3
- package/dist/prompts/context-builder.js +140 -19
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/disclaimer.d.ts +6 -0
- package/dist/prompts/disclaimer.js +9 -0
- package/dist/prompts/disclaimer.js.map +1 -0
- package/dist/prompts/policy-cards.d.ts +13 -0
- package/dist/prompts/policy-cards.js +197 -0
- package/dist/prompts/policy-cards.js.map +1 -0
- package/dist/prompts/sections.js +3 -3
- package/dist/prompts/sections.js.map +1 -1
- package/dist/prompts/workflow-prompts.d.ts +8 -0
- package/dist/prompts/workflow-prompts.js +208 -22
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/alpha-vantage.js +23 -1
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/sec-edgar.d.ts +8 -1
- package/dist/providers/sec-edgar.js +172 -5
- package/dist/providers/sec-edgar.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +2 -0
- package/dist/providers/yahoo-finance.js +203 -35
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.d.ts +3 -0
- package/dist/routing/classify-intent.js +82 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/defaults.js +4 -4
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/entity-extractor.d.ts +1 -0
- package/dist/routing/entity-extractor.js +158 -12
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/index.d.ts +10 -0
- package/dist/routing/index.js +7 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/legacy-rule-router.d.ts +9 -0
- package/dist/routing/legacy-rule-router.js +12 -0
- package/dist/routing/legacy-rule-router.js.map +1 -0
- package/dist/routing/planning.d.ts +54 -0
- package/dist/routing/planning.js +531 -0
- package/dist/routing/planning.js.map +1 -0
- package/dist/routing/route-manifest.d.ts +35 -0
- package/dist/routing/route-manifest.js +221 -0
- package/dist/routing/route-manifest.js.map +1 -0
- package/dist/routing/router-llm-client.d.ts +11 -0
- package/dist/routing/router-llm-client.js +42 -0
- package/dist/routing/router-llm-client.js.map +1 -0
- package/dist/routing/router-prompt.d.ts +2 -0
- package/dist/routing/router-prompt.js +141 -0
- package/dist/routing/router-prompt.js.map +1 -0
- package/dist/routing/router-types.d.ts +71 -0
- package/dist/routing/router-types.js +2 -0
- package/dist/routing/router-types.js.map +1 -0
- package/dist/routing/router.d.ts +11 -0
- package/dist/routing/router.js +638 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/routing/slot-resolver.js +46 -6
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/routing/turn-context.d.ts +44 -0
- package/dist/routing/turn-context.js +45 -0
- package/dist/routing/turn-context.js.map +1 -0
- package/dist/routing/types.d.ts +13 -1
- package/dist/runtime/answer-contracts.d.ts +82 -0
- package/dist/runtime/answer-contracts.js +414 -0
- package/dist/runtime/answer-contracts.js.map +1 -0
- package/dist/runtime/artifact-contracts.d.ts +14 -0
- package/dist/runtime/artifact-contracts.js +57 -0
- package/dist/runtime/artifact-contracts.js.map +1 -0
- package/dist/runtime/planning-evidence.d.ts +99 -0
- package/dist/runtime/planning-evidence.js +445 -0
- package/dist/runtime/planning-evidence.js.map +1 -0
- package/dist/runtime/session-coordinator.d.ts +81 -3
- package/dist/runtime/session-coordinator.js +201 -17
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
- package/dist/runtime/tool-defaults-wrapper.js +25 -0
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
- package/dist/sentiment/store.js +5 -0
- package/dist/sentiment/store.js.map +1 -1
- package/dist/system-prompt.js +23 -12
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +4 -4
- package/dist/tools/fundamentals/company-overview.d.ts +1 -1
- package/dist/tools/fundamentals/company-overview.js +1 -1
- package/dist/tools/fundamentals/company-overview.js.map +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -1
- package/dist/tools/fundamentals/comps.js +1 -1
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +1 -1
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +1 -1
- package/dist/tools/fundamentals/earnings.js +1 -1
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.d.ts +1 -1
- package/dist/tools/fundamentals/financials.js +1 -1
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +2 -1
- package/dist/tools/fundamentals/sec-filings.js +19 -2
- package/dist/tools/fundamentals/sec-filings.js.map +1 -1
- package/dist/tools/index.d.ts +29 -1
- package/dist/tools/index.js +30 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.d.ts +1 -1
- package/dist/tools/interaction/twitter-login.d.ts +1 -1
- package/dist/tools/macro/fear-greed.d.ts +1 -1
- package/dist/tools/macro/fear-greed.js +1 -1
- package/dist/tools/macro/fear-greed.js.map +1 -1
- package/dist/tools/macro/fred-data.d.ts +1 -1
- package/dist/tools/macro/fred-data.js +29 -5
- package/dist/tools/macro/fred-data.js.map +1 -1
- package/dist/tools/market/crypto-history.d.ts +1 -1
- package/dist/tools/market/crypto-history.js +18 -2
- package/dist/tools/market/crypto-history.js.map +1 -1
- package/dist/tools/market/crypto-price.d.ts +1 -1
- package/dist/tools/market/crypto-price.js +1 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/search-ticker.d.ts +1 -1
- package/dist/tools/market/search-ticker.js +1 -1
- package/dist/tools/market/search-ticker.js.map +1 -1
- package/dist/tools/market/stock-history.d.ts +1 -1
- package/dist/tools/market/stock-history.js +1 -1
- package/dist/tools/market/stock-history.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +1 -1
- package/dist/tools/market/stock-quote.js +1 -1
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/greeks.js +0 -1
- package/dist/tools/options/greeks.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +1 -1
- package/dist/tools/options/option-chain.js +13 -5
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/correlation.js +1 -1
- package/dist/tools/portfolio/correlation.js.map +1 -1
- package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
- package/dist/tools/portfolio/holdings-overlap.js +105 -0
- package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
- package/dist/tools/portfolio/predictions.d.ts +1 -1
- package/dist/tools/portfolio/predictions.js +1 -1
- package/dist/tools/portfolio/predictions.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/portfolio/tracker.d.ts +1 -1
- package/dist/tools/portfolio/tracker.js +1 -1
- package/dist/tools/portfolio/tracker.js.map +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +1 -1
- package/dist/tools/portfolio/watchlist.js +12 -4
- package/dist/tools/portfolio/watchlist.js.map +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-summary.js +57 -2
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-search.d.ts +1 -1
- package/dist/tools/sentiment/web-search.js +32 -3
- package/dist/tools/sentiment/web-search.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/web-sentiment.js +1 -1
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +3 -3
- package/dist/tools/technical/backtest.js +41 -27
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.d.ts +1 -1
- package/dist/tools/technical/indicators.js +8 -4
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/options.d.ts +10 -0
- package/dist/types/portfolio.d.ts +27 -0
- package/dist/workflows/compare-assets.js +38 -2
- package/dist/workflows/compare-assets.js.map +1 -1
- package/dist/workflows/options-screener.js +94 -8
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +9 -5
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/ask-user-bridge.ts +82 -0
- package/gui/server/background-quotes.ts +31 -0
- package/gui/server/chat-event-adapter.ts +142 -0
- package/gui/server/gui-session-manager.ts +5 -0
- package/gui/server/invoke-tool.ts +89 -0
- package/gui/server/live-chat-event-adapter.ts +181 -0
- package/gui/server/model-setup.ts +100 -0
- package/gui/server/package.json +5 -0
- package/gui/server/projector.ts +254 -0
- package/gui/server/prompt-observation.ts +61 -0
- package/gui/server/server.ts +703 -0
- package/gui/server/session-actions.ts +31 -0
- package/gui/server/session-entry-wait.ts +81 -0
- package/gui/server/tool-metadata.ts +88 -0
- package/gui/server/websocket.ts +128 -0
- package/gui/server/writer-lock.ts +118 -0
- package/gui/shared/chat-events.ts +118 -0
- package/gui/shared/event-reducer.ts +186 -0
- package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +1 -0
- package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
- package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
- package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
- package/gui/web/dist/index.html +17 -0
- package/package.json +50 -18
- package/src/analysts/contracts.ts +189 -0
- package/src/analysts/orchestrator.ts +300 -0
- package/src/cli.ts +206 -0
- package/src/config.ts +245 -0
- package/src/index.ts +5 -0
- package/src/infra/browser.ts +111 -0
- package/src/infra/cache.ts +103 -0
- package/src/infra/http-client.ts +68 -0
- package/src/infra/index.ts +18 -0
- package/src/infra/native-dependencies.ts +12 -0
- package/src/infra/node-version.ts +24 -0
- package/src/infra/open-url.ts +28 -0
- package/src/infra/opencandle-paths.ts +64 -0
- package/src/infra/rate-limiter.ts +73 -0
- package/src/memory/index.ts +10 -0
- package/src/memory/manager.ts +192 -0
- package/src/memory/preference-extractor.ts +106 -0
- package/src/memory/retrieval.ts +70 -0
- package/src/memory/sqlite.ts +172 -0
- package/src/memory/storage.ts +205 -0
- package/src/memory/tool-defaults.ts +87 -0
- package/src/memory/types.ts +71 -0
- package/src/onboarding/connect.ts +184 -0
- package/src/onboarding/credential-interceptor.ts +134 -0
- package/src/onboarding/degradation-accumulator.ts +79 -0
- package/src/onboarding/prompt-user.ts +85 -0
- package/src/onboarding/providers.ts +315 -0
- package/src/onboarding/state.ts +218 -0
- package/src/onboarding/tool-helpers.ts +111 -0
- package/src/onboarding/tool-tags.ts +201 -0
- package/src/onboarding/validation.ts +158 -0
- package/src/pi/opencandle-extension.ts +955 -0
- package/src/pi/session-storage.ts +5 -0
- package/src/pi/session.ts +81 -0
- package/src/pi/setup.ts +381 -0
- package/src/pi/tool-adapter.ts +36 -0
- package/src/prompts/context-builder.ts +315 -0
- package/src/prompts/disclaimer.ts +9 -0
- package/src/prompts/policy-cards.ts +220 -0
- package/src/prompts/sections.ts +46 -0
- package/src/prompts/workflow-prompts.ts +433 -0
- package/src/providers/alpha-vantage.ts +315 -0
- package/src/providers/coingecko.ts +96 -0
- package/src/providers/exa-search.ts +373 -0
- package/src/providers/fear-greed.ts +45 -0
- package/src/providers/finnhub.ts +124 -0
- package/src/providers/fred.ts +83 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider-credential-error.ts +23 -0
- package/src/providers/reddit.ts +151 -0
- package/src/providers/sec-edgar.ts +312 -0
- package/src/providers/twitter.ts +173 -0
- package/src/providers/web-search.ts +293 -0
- package/src/providers/with-fallback.ts +41 -0
- package/src/providers/wrap-provider.ts +64 -0
- package/src/providers/yahoo-finance.ts +534 -0
- package/src/routing/classify-intent.ts +285 -0
- package/src/routing/defaults.ts +29 -0
- package/src/routing/entity-extractor.ts +291 -0
- package/src/routing/index.ts +70 -0
- package/src/routing/legacy-rule-router.ts +13 -0
- package/src/routing/planning.ts +732 -0
- package/src/routing/route-manifest.ts +287 -0
- package/src/routing/router-llm-client.ts +51 -0
- package/src/routing/router-prompt.ts +163 -0
- package/src/routing/router-types.ts +87 -0
- package/src/routing/router.ts +712 -0
- package/src/routing/slot-resolver.ts +190 -0
- package/src/routing/turn-context.ts +111 -0
- package/src/routing/types.ts +75 -0
- package/src/runtime/answer-contracts.ts +633 -0
- package/src/runtime/artifact-contracts.ts +76 -0
- package/src/runtime/evidence.ts +77 -0
- package/src/runtime/planning-evidence.ts +591 -0
- package/src/runtime/prompt-step.ts +75 -0
- package/src/runtime/provider-tracker.ts +40 -0
- package/src/runtime/run-context.ts +22 -0
- package/src/runtime/session-coordinator.ts +472 -0
- package/src/runtime/tool-defaults-wrapper.ts +35 -0
- package/src/runtime/validation.ts +214 -0
- package/src/runtime/workflow-events.ts +75 -0
- package/src/runtime/workflow-runner.ts +188 -0
- package/src/runtime/workflow-types.ts +102 -0
- package/src/sentiment/adapters/finnhub.ts +44 -0
- package/src/sentiment/adapters/reddit.ts +65 -0
- package/src/sentiment/adapters/twitter.ts +36 -0
- package/src/sentiment/adapters/web.ts +44 -0
- package/src/sentiment/index.ts +58 -0
- package/src/sentiment/keywords.ts +9 -0
- package/src/sentiment/pipeline.ts +68 -0
- package/src/sentiment/scorer.ts +78 -0
- package/src/sentiment/store.ts +260 -0
- package/src/sentiment/trends.ts +90 -0
- package/src/sentiment/types.ts +108 -0
- package/src/system-prompt.ts +118 -0
- package/src/tool-kit.ts +68 -0
- package/src/tools/AGENTS.md +36 -0
- package/src/tools/fundamentals/company-overview.ts +54 -0
- package/src/tools/fundamentals/comps.ts +156 -0
- package/src/tools/fundamentals/dcf.ts +267 -0
- package/src/tools/fundamentals/earnings.ts +47 -0
- package/src/tools/fundamentals/financials.ts +54 -0
- package/src/tools/fundamentals/sec-filings.ts +84 -0
- package/src/tools/index.ts +91 -0
- package/src/tools/interaction/ask-user.ts +81 -0
- package/src/tools/interaction/twitter-login.ts +93 -0
- package/src/tools/macro/fear-greed.ts +41 -0
- package/src/tools/macro/fred-data.ts +80 -0
- package/src/tools/market/crypto-history.ts +67 -0
- package/src/tools/market/crypto-price.ts +53 -0
- package/src/tools/market/search-ticker.ts +53 -0
- package/src/tools/market/stock-history.ts +79 -0
- package/src/tools/market/stock-quote.ts +64 -0
- package/src/tools/options/greeks.ts +81 -0
- package/src/tools/options/option-chain.ts +96 -0
- package/src/tools/portfolio/correlation.ts +162 -0
- package/src/tools/portfolio/holdings-overlap.ts +123 -0
- package/src/tools/portfolio/predictions.ts +253 -0
- package/src/tools/portfolio/risk-analysis.ts +134 -0
- package/src/tools/portfolio/tracker.ts +147 -0
- package/src/tools/portfolio/watchlist.ts +159 -0
- package/src/tools/sentiment/reddit-sentiment.ts +164 -0
- package/src/tools/sentiment/sentiment-summary.ts +316 -0
- package/src/tools/sentiment/sentiment-trend.ts +58 -0
- package/src/tools/sentiment/twitter-sentiment.ts +96 -0
- package/src/tools/sentiment/web-search.ts +183 -0
- package/src/tools/sentiment/web-sentiment.ts +76 -0
- package/src/tools/technical/backtest.ts +267 -0
- package/src/tools/technical/indicators.ts +256 -0
- package/src/types/fundamentals.ts +46 -0
- package/src/types/index.ts +20 -0
- package/src/types/macro.ts +27 -0
- package/src/types/market.ts +43 -0
- package/src/types/options.ts +52 -0
- package/src/types/portfolio.ts +73 -0
- package/src/types/sentiment.ts +70 -0
- package/src/workflows/compare-assets.ts +75 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/options-screener.ts +127 -0
- package/src/workflows/portfolio-builder.ts +56 -0
- package/src/workflows/types.ts +4 -0
- package/dist/runtime/index.d.ts +0 -16
- package/dist/runtime/index.js +0 -10
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/provider-ids.d.ts +0 -14
- package/dist/runtime/provider-ids.js +0 -14
- package/dist/runtime/provider-ids.js.map +0 -1
|
@@ -0,0 +1,534 @@
|
|
|
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, OptionsMarketSession, OptionsQuoteStatus } from "../types/options.js";
|
|
7
|
+
import type { FundHoldings } from "../types/portfolio.js";
|
|
8
|
+
import { computeGreeks } from "../tools/options/greeks.js";
|
|
9
|
+
|
|
10
|
+
const BASE_URL = "https://query1.finance.yahoo.com/v8/finance/chart";
|
|
11
|
+
const QUOTE_SUMMARY_URL = "https://query1.finance.yahoo.com/v10/finance/quoteSummary";
|
|
12
|
+
|
|
13
|
+
interface YahooChartResponse {
|
|
14
|
+
chart: {
|
|
15
|
+
result: Array<{
|
|
16
|
+
meta: Record<string, any>;
|
|
17
|
+
timestamp: number[];
|
|
18
|
+
indicators: {
|
|
19
|
+
quote: Array<{
|
|
20
|
+
open: number[];
|
|
21
|
+
high: number[];
|
|
22
|
+
low: number[];
|
|
23
|
+
close: number[];
|
|
24
|
+
volume: number[];
|
|
25
|
+
}>;
|
|
26
|
+
adjclose?: Array<{ adjclose: number[] }>;
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
29
|
+
error?: { code: string; description: string };
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface YahooQuoteSummaryResponse {
|
|
34
|
+
quoteSummary: {
|
|
35
|
+
result?: Array<{
|
|
36
|
+
price?: {
|
|
37
|
+
symbol?: string;
|
|
38
|
+
shortName?: string;
|
|
39
|
+
longName?: string;
|
|
40
|
+
};
|
|
41
|
+
topHoldings?: {
|
|
42
|
+
holdings?: Array<{
|
|
43
|
+
symbol?: string;
|
|
44
|
+
holdingName?: string;
|
|
45
|
+
holdingPercent?: number;
|
|
46
|
+
}>;
|
|
47
|
+
equityHoldings?: {
|
|
48
|
+
sectorWeightings?: Array<Record<string, number>>;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}>;
|
|
52
|
+
error?: { code?: string; description?: string } | null;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getQuote(symbol: string): Promise<StockQuote> {
|
|
57
|
+
const cacheKey = `yahoo:quote:${symbol}`;
|
|
58
|
+
const cached = cache.get<StockQuote>(cacheKey);
|
|
59
|
+
if (cached) return cached;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await rateLimiter.acquire("yahoo");
|
|
63
|
+
|
|
64
|
+
const url = `${BASE_URL}/${encodeURIComponent(symbol)}?interval=1d&range=1d`;
|
|
65
|
+
const data = await httpGet<YahooChartResponse>(url, {
|
|
66
|
+
headers: { "User-Agent": "OpenCandle/1.0" },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (data.chart.error) {
|
|
70
|
+
throw new Error(`Yahoo Finance: ${data.chart.error.description}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = data.chart.result[0];
|
|
74
|
+
const meta = result.meta;
|
|
75
|
+
const indicators = result.indicators.quote[0];
|
|
76
|
+
|
|
77
|
+
const price = meta.regularMarketPrice ?? 0;
|
|
78
|
+
const prevClose = meta.chartPreviousClose ?? meta.previousClose ?? price;
|
|
79
|
+
const change = price - prevClose;
|
|
80
|
+
const changePercent = prevClose !== 0 ? (change / prevClose) * 100 : 0;
|
|
81
|
+
|
|
82
|
+
// Open price: try meta first, fall back to indicators
|
|
83
|
+
const open = meta.regularMarketOpen ?? indicators?.open?.[0] ?? price;
|
|
84
|
+
|
|
85
|
+
const quote: StockQuote = {
|
|
86
|
+
symbol: meta.symbol,
|
|
87
|
+
price,
|
|
88
|
+
change,
|
|
89
|
+
changePercent,
|
|
90
|
+
open,
|
|
91
|
+
high: meta.regularMarketDayHigh ?? indicators?.high?.[0] ?? price,
|
|
92
|
+
low: meta.regularMarketDayLow ?? indicators?.low?.[0] ?? price,
|
|
93
|
+
previousClose: prevClose,
|
|
94
|
+
volume: meta.regularMarketVolume ?? 0,
|
|
95
|
+
marketCap: meta.marketCap ?? 0,
|
|
96
|
+
pe: null, // Not in chart endpoint
|
|
97
|
+
week52High: meta.fiftyTwoWeekHigh ?? 0,
|
|
98
|
+
week52Low: meta.fiftyTwoWeekLow ?? 0,
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
cache.set(cacheKey, quote, TTL.QUOTE);
|
|
103
|
+
return quote;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const stale = cache.getStale<StockQuote>(cacheKey, STALE_LIMIT.QUOTE);
|
|
106
|
+
if (stale) return stale.value;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function getHistory(
|
|
112
|
+
symbol: string,
|
|
113
|
+
range: string = "6mo",
|
|
114
|
+
interval: string = "1d",
|
|
115
|
+
): Promise<OHLCV[]> {
|
|
116
|
+
const cacheKey = `yahoo:history:${symbol}:${range}:${interval}`;
|
|
117
|
+
const cached = cache.get<OHLCV[]>(cacheKey);
|
|
118
|
+
if (cached) return cached;
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await rateLimiter.acquire("yahoo");
|
|
122
|
+
|
|
123
|
+
const url = `${BASE_URL}/${encodeURIComponent(symbol)}?interval=${interval}&range=${range}`;
|
|
124
|
+
const data = await httpGet<YahooChartResponse>(url, {
|
|
125
|
+
headers: { "User-Agent": "OpenCandle/1.0" },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (data.chart.error) {
|
|
129
|
+
throw new Error(`Yahoo Finance: ${data.chart.error.description}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = data.chart.result[0];
|
|
133
|
+
const timestamps = result.timestamp;
|
|
134
|
+
const quotes = result.indicators.quote[0];
|
|
135
|
+
|
|
136
|
+
const ohlcv: OHLCV[] = timestamps
|
|
137
|
+
.map((ts, i) => ({
|
|
138
|
+
date: new Date(ts * 1000).toISOString().split("T")[0],
|
|
139
|
+
open: quotes.open[i],
|
|
140
|
+
high: quotes.high[i],
|
|
141
|
+
low: quotes.low[i],
|
|
142
|
+
close: quotes.close[i],
|
|
143
|
+
volume: quotes.volume[i],
|
|
144
|
+
}))
|
|
145
|
+
.filter((bar) => bar.open != null && bar.close != null);
|
|
146
|
+
|
|
147
|
+
cache.set(cacheKey, ohlcv, TTL.HISTORY);
|
|
148
|
+
return ohlcv;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const stale = cache.getStale<OHLCV[]>(cacheKey, STALE_LIMIT.HISTORY);
|
|
151
|
+
if (stale) return stale.value;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function getFundHoldings(symbol: string): Promise<FundHoldings> {
|
|
157
|
+
const normalizedSymbol = symbol.toUpperCase();
|
|
158
|
+
const cacheKey = `yahoo:fund-holdings:${normalizedSymbol}`;
|
|
159
|
+
const cached = cache.get<FundHoldings>(cacheKey);
|
|
160
|
+
if (cached) return cached;
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await rateLimiter.acquire("yahoo");
|
|
164
|
+
|
|
165
|
+
const modules = encodeURIComponent("price,topHoldings");
|
|
166
|
+
const url = `${QUOTE_SUMMARY_URL}/${encodeURIComponent(normalizedSymbol)}?modules=${modules}`;
|
|
167
|
+
const data = await httpGet<YahooQuoteSummaryResponse>(url, {
|
|
168
|
+
headers: { "User-Agent": "OpenCandle/1.0" },
|
|
169
|
+
});
|
|
170
|
+
const result = data.quoteSummary.result?.[0];
|
|
171
|
+
if (data.quoteSummary.error) {
|
|
172
|
+
throw new Error(`Yahoo Finance: ${data.quoteSummary.error.description ?? data.quoteSummary.error.code ?? "quoteSummary error"}`);
|
|
173
|
+
}
|
|
174
|
+
if (!result?.topHoldings?.holdings?.length) {
|
|
175
|
+
throw new Error(`Yahoo Finance: no fund holdings returned for ${normalizedSymbol}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const holdings: FundHoldings = {
|
|
179
|
+
symbol: result.price?.symbol?.toUpperCase() ?? normalizedSymbol,
|
|
180
|
+
name: result.price?.shortName ?? result.price?.longName,
|
|
181
|
+
provider: "yahoo",
|
|
182
|
+
holdings: result.topHoldings.holdings.flatMap((holding) => {
|
|
183
|
+
const holdingSymbol = holding.symbol?.trim().toUpperCase();
|
|
184
|
+
const weight = normalizeHoldingWeight(holding.holdingPercent);
|
|
185
|
+
if (!holdingSymbol || weight === undefined) return [];
|
|
186
|
+
return [{
|
|
187
|
+
symbol: holdingSymbol,
|
|
188
|
+
name: holding.holdingName?.trim() || holdingSymbol,
|
|
189
|
+
weight,
|
|
190
|
+
}];
|
|
191
|
+
}),
|
|
192
|
+
sectorWeights: normalizeSectorWeights(result.topHoldings.equityHoldings?.sectorWeightings),
|
|
193
|
+
};
|
|
194
|
+
if (holdings.holdings.length === 0) {
|
|
195
|
+
throw new Error(`Yahoo Finance: no weighted fund holdings returned for ${normalizedSymbol}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
cache.set(cacheKey, holdings, TTL.FUNDAMENTALS);
|
|
199
|
+
return holdings;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const stale = cache.getStale<FundHoldings>(cacheKey, STALE_LIMIT.FUNDAMENTALS);
|
|
202
|
+
if (stale) return stale.value;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeHoldingWeight(value: number | undefined): number | undefined {
|
|
208
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) return undefined;
|
|
209
|
+
return value > 1 ? roundWeight(value / 100) : roundWeight(value);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeSectorWeights(
|
|
213
|
+
sectors: Array<Record<string, number>> | undefined,
|
|
214
|
+
): Record<string, number> | undefined {
|
|
215
|
+
if (!sectors?.length) return undefined;
|
|
216
|
+
const weights: Record<string, number> = {};
|
|
217
|
+
for (const sector of sectors) {
|
|
218
|
+
for (const [name, rawWeight] of Object.entries(sector)) {
|
|
219
|
+
const weight = normalizeHoldingWeight(rawWeight);
|
|
220
|
+
if (weight !== undefined) weights[name] = weight;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return Object.keys(weights).length > 0 ? weights : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function roundWeight(value: number): number {
|
|
227
|
+
return Math.round(value * 10_000) / 10_000;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// --- Options Chain (v7 API with crumb+cookie auth) ---
|
|
231
|
+
|
|
232
|
+
const BROWSER_UA =
|
|
233
|
+
"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";
|
|
234
|
+
|
|
235
|
+
let cachedCrumb: { crumb: string; cookie: string; expiresAt: number } | null = null;
|
|
236
|
+
|
|
237
|
+
export function clearCrumbCache(): void {
|
|
238
|
+
cachedCrumb = null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function getYahooCrumb(): Promise<{ crumb: string; cookie: string }> {
|
|
242
|
+
if (cachedCrumb && Date.now() < cachedCrumb.expiresAt) {
|
|
243
|
+
return { crumb: cachedCrumb.crumb, cookie: cachedCrumb.cookie };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Step 1: Hit fc.yahoo.com to get a session cookie
|
|
247
|
+
const cookieRes = await fetch("https://fc.yahoo.com/t", {
|
|
248
|
+
headers: { "User-Agent": BROWSER_UA },
|
|
249
|
+
});
|
|
250
|
+
const setCookie = cookieRes.headers.get("set-cookie") ?? "";
|
|
251
|
+
const cookie = setCookie.split(";")[0]; // Extract just the cookie value
|
|
252
|
+
|
|
253
|
+
// Step 2: Use the cookie to get a crumb
|
|
254
|
+
const crumbRes = await fetch("https://query2.finance.yahoo.com/v1/test/getcrumb", {
|
|
255
|
+
headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
|
|
256
|
+
});
|
|
257
|
+
const crumb = await crumbRes.text();
|
|
258
|
+
|
|
259
|
+
if (!crumb || crumb.includes("Unauthorized")) {
|
|
260
|
+
throw new Error("Failed to acquire Yahoo Finance crumb");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
cachedCrumb = { crumb, cookie, expiresAt: Date.now() + TTL.CRUMB };
|
|
264
|
+
return { crumb, cookie };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
interface YahooOptionsResponse {
|
|
268
|
+
optionChain: {
|
|
269
|
+
result: Array<{
|
|
270
|
+
underlyingSymbol: string;
|
|
271
|
+
expirationDates: number[];
|
|
272
|
+
strikes: number[];
|
|
273
|
+
quote: Record<string, any>;
|
|
274
|
+
options: Array<{
|
|
275
|
+
expirationDate: number;
|
|
276
|
+
calls: any[];
|
|
277
|
+
puts: any[];
|
|
278
|
+
}>;
|
|
279
|
+
}>;
|
|
280
|
+
error?: any;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export async function getOptionsChain(
|
|
285
|
+
symbol: string,
|
|
286
|
+
expiration?: number,
|
|
287
|
+
): Promise<OptionsChain> {
|
|
288
|
+
const cacheKey = `yahoo:options:${symbol}:${expiration ?? "nearest"}`;
|
|
289
|
+
const cached = cache.get<OptionsChain>(cacheKey);
|
|
290
|
+
if (cached) return cached;
|
|
291
|
+
|
|
292
|
+
await rateLimiter.acquire("yahoo");
|
|
293
|
+
|
|
294
|
+
const { crumb, cookie } = await getYahooCrumb();
|
|
295
|
+
const dateParam = expiration ? `&date=${expiration}` : "";
|
|
296
|
+
const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
|
|
297
|
+
|
|
298
|
+
let res: Response | null = null;
|
|
299
|
+
let fetchError: unknown;
|
|
300
|
+
try {
|
|
301
|
+
res = await fetch(url, {
|
|
302
|
+
headers: { "User-Agent": BROWSER_UA, Cookie: cookie },
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
fetchError = error;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// On 401 or 429, refresh crumb and retry once
|
|
309
|
+
if (res?.status === 401 || res?.status === 429) {
|
|
310
|
+
try {
|
|
311
|
+
clearCrumbCache();
|
|
312
|
+
const fresh = await getYahooCrumb();
|
|
313
|
+
const retryUrl = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(fresh.crumb)}${dateParam}`;
|
|
314
|
+
res = await fetch(retryUrl, {
|
|
315
|
+
headers: { "User-Agent": BROWSER_UA, Cookie: fresh.cookie },
|
|
316
|
+
});
|
|
317
|
+
} catch (error) {
|
|
318
|
+
fetchError = error;
|
|
319
|
+
res = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// If still failing, fall back to stealth browser (bypasses TLS fingerprinting)
|
|
324
|
+
if (!res?.ok) {
|
|
325
|
+
let browserError: unknown;
|
|
326
|
+
try {
|
|
327
|
+
const browserData = await fetchOptionsViaBrowser(symbol, expiration);
|
|
328
|
+
if (browserData) {
|
|
329
|
+
const chain = parseOptionsResponse(browserData);
|
|
330
|
+
cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
|
|
331
|
+
return chain;
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
browserError = error;
|
|
335
|
+
}
|
|
336
|
+
// All fetches failed — try stale cache before giving up
|
|
337
|
+
const stale = cache.getStale<OptionsChain>(cacheKey, STALE_LIMIT.OPTIONS_CHAIN);
|
|
338
|
+
if (stale) return stale.value;
|
|
339
|
+
if (res) {
|
|
340
|
+
const message = `Yahoo Finance options: HTTP ${res.status}`;
|
|
341
|
+
if (browserError instanceof Error) {
|
|
342
|
+
throw new Error(`${message}; browser fallback failed: ${browserError.message}`);
|
|
343
|
+
}
|
|
344
|
+
throw new Error(message);
|
|
345
|
+
}
|
|
346
|
+
if (browserError instanceof Error) {
|
|
347
|
+
const message = fetchError instanceof Error ? fetchError.message : "Yahoo Finance options: fetch failed";
|
|
348
|
+
throw new Error(`${message}; browser fallback failed: ${browserError.message}`);
|
|
349
|
+
}
|
|
350
|
+
throw fetchError instanceof Error ? fetchError : new Error("Yahoo Finance options: fetch failed");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const data: YahooOptionsResponse = await res.json();
|
|
354
|
+
const chain = parseOptionsResponse(data);
|
|
355
|
+
cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
|
|
356
|
+
return chain;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Compute time to expiry in years from a Yahoo expiration timestamp (midnight UTC).
|
|
361
|
+
* US equity options expire at 4:00 PM ET. During EDT that is 20:00 UTC.
|
|
362
|
+
* We use 21:00 UTC (4 PM EST / 5 PM EDT) as a conservative close offset
|
|
363
|
+
* and apply a floor of ~1 hour to prevent numerical instability near expiry.
|
|
364
|
+
*/
|
|
365
|
+
export function computeTimeToExpiry(expirationTs: number, nowMs: number = Date.now()): number {
|
|
366
|
+
const MARKET_CLOSE_OFFSET_S = 21 * 3600; // 21:00 UTC ≈ 4 PM ET
|
|
367
|
+
const MIN_TIME_YEARS = 1 / (365 * 24); // ~1 hour floor
|
|
368
|
+
const SECONDS_PER_YEAR = 365 * 24 * 3600;
|
|
369
|
+
|
|
370
|
+
const expiryCloseTs = expirationTs + MARKET_CLOSE_OFFSET_S;
|
|
371
|
+
const remainingS = expiryCloseTs - nowMs / 1000;
|
|
372
|
+
|
|
373
|
+
if (remainingS <= 0) return 0;
|
|
374
|
+
return Math.max(MIN_TIME_YEARS, remainingS / SECONDS_PER_YEAR);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getUsOptionsMarketSession(now: Date = new Date()): OptionsMarketSession {
|
|
378
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
379
|
+
timeZone: "America/New_York",
|
|
380
|
+
weekday: "short",
|
|
381
|
+
hour: "2-digit",
|
|
382
|
+
minute: "2-digit",
|
|
383
|
+
hour12: false,
|
|
384
|
+
}).formatToParts(now);
|
|
385
|
+
const part = (type: string): string => parts.find((p) => p.type === type)?.value ?? "";
|
|
386
|
+
const weekday = part("weekday");
|
|
387
|
+
if (weekday === "Sat" || weekday === "Sun") return "closed";
|
|
388
|
+
|
|
389
|
+
const hour = Number(part("hour"));
|
|
390
|
+
const minute = Number(part("minute"));
|
|
391
|
+
const minutes = hour * 60 + minute;
|
|
392
|
+
if (minutes < 9 * 60 + 30) return "pre_market";
|
|
393
|
+
if (minutes < 16 * 60) return "regular";
|
|
394
|
+
return "after_hours";
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function buildOptionsQuoteStatus(
|
|
398
|
+
contracts: OptionContract[],
|
|
399
|
+
now: Date = new Date(),
|
|
400
|
+
): OptionsQuoteStatus {
|
|
401
|
+
const marketSession = getUsOptionsMarketSession(now);
|
|
402
|
+
const totalContracts = contracts.length;
|
|
403
|
+
const zeroBidAskContracts = contracts.filter((c) => c.bid === 0 && c.ask === 0).length;
|
|
404
|
+
const allZeroBidAsk = totalContracts > 0 && zeroBidAskContracts === totalContracts;
|
|
405
|
+
const hasLiveBidAsk = contracts.some((c) => c.bid > 0 || c.ask > 0);
|
|
406
|
+
|
|
407
|
+
if (allZeroBidAsk && marketSession !== "regular") {
|
|
408
|
+
return {
|
|
409
|
+
marketSession,
|
|
410
|
+
bidAskState: "closed_market_or_stale_quotes",
|
|
411
|
+
zeroBidAskContracts,
|
|
412
|
+
totalContracts,
|
|
413
|
+
warning:
|
|
414
|
+
"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.",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (allZeroBidAsk) {
|
|
419
|
+
return {
|
|
420
|
+
marketSession,
|
|
421
|
+
bidAskState: "live_zero_bid_ask",
|
|
422
|
+
zeroBidAskContracts,
|
|
423
|
+
totalContracts,
|
|
424
|
+
warning:
|
|
425
|
+
"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.",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
marketSession,
|
|
431
|
+
bidAskState: hasLiveBidAsk ? "live_quotes" : "mixed_or_unknown",
|
|
432
|
+
zeroBidAskContracts,
|
|
433
|
+
totalContracts,
|
|
434
|
+
...(marketSession !== "regular"
|
|
435
|
+
? {
|
|
436
|
+
warning:
|
|
437
|
+
"Options bid/ask quotes may be stale outside regular options trading hours; verify live executable prices after the market opens.",
|
|
438
|
+
}
|
|
439
|
+
: {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function parseOptionsResponse(data: YahooOptionsResponse): OptionsChain {
|
|
444
|
+
if (data.optionChain.error) {
|
|
445
|
+
throw new Error(`Yahoo Finance options: ${JSON.stringify(data.optionChain.error)}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const result = data.optionChain.result[0];
|
|
449
|
+
const quote = result.quote;
|
|
450
|
+
const underlyingPrice = quote.regularMarketPrice ?? 0;
|
|
451
|
+
const opts = result.options[0];
|
|
452
|
+
const riskFreeRate = 0.05;
|
|
453
|
+
|
|
454
|
+
const expirationTs = opts.expirationDate;
|
|
455
|
+
const expirationDate = new Date(expirationTs * 1000).toISOString().split("T")[0];
|
|
456
|
+
const timeYears = computeTimeToExpiry(expirationTs);
|
|
457
|
+
|
|
458
|
+
const mapContract = (c: any, type: "call" | "put"): OptionContract => {
|
|
459
|
+
const strike = c.strike ?? c.strike?.raw ?? 0;
|
|
460
|
+
const iv = c.impliedVolatility ?? c.impliedVolatility?.raw ?? 0;
|
|
461
|
+
const greeks = computeGreeks({ type, spot: underlyingPrice, strike, timeYears, iv, riskFreeRate });
|
|
462
|
+
return {
|
|
463
|
+
contractSymbol: c.contractSymbol ?? "",
|
|
464
|
+
type,
|
|
465
|
+
strike,
|
|
466
|
+
expiration: expirationDate,
|
|
467
|
+
bid: c.bid ?? c.bid?.raw ?? 0,
|
|
468
|
+
ask: c.ask ?? c.ask?.raw ?? 0,
|
|
469
|
+
lastPrice: c.lastPrice ?? c.lastPrice?.raw ?? 0,
|
|
470
|
+
volume: c.volume ?? c.volume?.raw ?? 0,
|
|
471
|
+
openInterest: c.openInterest ?? c.openInterest?.raw ?? 0,
|
|
472
|
+
impliedVolatility: iv,
|
|
473
|
+
inTheMoney: c.inTheMoney ?? false,
|
|
474
|
+
greeks,
|
|
475
|
+
};
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const calls = (opts.calls ?? []).map((c: any) => mapContract(c, "call"));
|
|
479
|
+
const puts = (opts.puts ?? []).map((c: any) => mapContract(c, "put"));
|
|
480
|
+
const totalCallVolume = calls.reduce((s, c) => s + c.volume, 0);
|
|
481
|
+
const totalPutVolume = puts.reduce((s, c) => s + c.volume, 0);
|
|
482
|
+
const quoteStatus = buildOptionsQuoteStatus([...calls, ...puts]);
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
symbol: result.underlyingSymbol,
|
|
486
|
+
underlyingPrice,
|
|
487
|
+
expirationDate,
|
|
488
|
+
expirationDates: result.expirationDates.map((ts) => new Date(ts * 1000).toISOString().split("T")[0]),
|
|
489
|
+
calls,
|
|
490
|
+
puts,
|
|
491
|
+
totalCallVolume,
|
|
492
|
+
totalPutVolume,
|
|
493
|
+
putCallRatio: totalCallVolume > 0 ? totalPutVolume / totalCallVolume : 0,
|
|
494
|
+
quoteStatus,
|
|
495
|
+
fetchedAt: new Date().toISOString(),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Fallback: fetch options data via Camoufox stealth browser.
|
|
501
|
+
* Bypasses Yahoo's TLS fingerprinting and rate limiting.
|
|
502
|
+
*/
|
|
503
|
+
async function fetchOptionsViaBrowser(
|
|
504
|
+
symbol: string,
|
|
505
|
+
expiration?: number,
|
|
506
|
+
): Promise<YahooOptionsResponse | null> {
|
|
507
|
+
try {
|
|
508
|
+
// Avoid loading the script-heavy Yahoo Finance homepage: Playwright 1.60
|
|
509
|
+
// can crash on some pageerror payloads emitted by finance.yahoo.com.
|
|
510
|
+
// Navigating directly to Yahoo's JSON endpoints still uses the browser's
|
|
511
|
+
// cookies/TLS fingerprint without requiring cross-origin fetch from page JS.
|
|
512
|
+
const dateParam = expiration ? `&date=${expiration}` : "";
|
|
513
|
+
return await StealthBrowser.run(async (page) => {
|
|
514
|
+
await page.goto("https://query2.finance.yahoo.com/v1/test/getcrumb", {
|
|
515
|
+
waitUntil: "domcontentloaded",
|
|
516
|
+
timeout: 15000,
|
|
517
|
+
});
|
|
518
|
+
const crumb = (await page.locator("body").innerText()).trim();
|
|
519
|
+
if (!crumb) return null;
|
|
520
|
+
|
|
521
|
+
const url = `https://query1.finance.yahoo.com/v7/finance/options/${encodeURIComponent(symbol)}?crumb=${encodeURIComponent(crumb)}${dateParam}`;
|
|
522
|
+
const response = await page.goto(url, {
|
|
523
|
+
waitUntil: "domcontentloaded",
|
|
524
|
+
timeout: 15000,
|
|
525
|
+
});
|
|
526
|
+
if (!response?.ok()) return null;
|
|
527
|
+
|
|
528
|
+
const text = (await page.locator("body").innerText()).trim();
|
|
529
|
+
return JSON.parse(text) as YahooOptionsResponse;
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
533
|
+
}
|
|
534
|
+
}
|