opencandle 0.4.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/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -3
- package/dist/config.js +61 -2
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +1 -3
- package/dist/infra/browser.js +1 -1
- package/dist/infra/browser.js.map +1 -1
- 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/manager.d.ts +9 -0
- package/dist/memory/manager.js +28 -11
- package/dist/memory/manager.js.map +1 -1
- package/dist/memory/storage.d.ts +3 -2
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/types.js +4 -0
- package/dist/memory/types.js.map +1 -1
- package/dist/pi/opencandle-extension.js +230 -36
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/setup.js +10 -0
- package/dist/pi/setup.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +18 -3
- package/dist/prompts/context-builder.js +102 -16
- 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.js +3 -3
- package/dist/prompts/sections.js.map +1 -1
- package/dist/prompts/workflow-prompts.js +170 -18
- 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 +134 -3
- 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 +3 -3
- 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 +7 -1
- package/dist/routing/index.js +4 -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-prompt.js +45 -42
- package/dist/routing/router-prompt.js.map +1 -1
- package/dist/routing/router-types.d.ts +9 -0
- package/dist/routing/router.d.ts +1 -0
- package/dist/routing/router.js +456 -12
- package/dist/routing/router.js.map +1 -1
- 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 +20 -2
- package/dist/runtime/session-coordinator.js +47 -14
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/system-prompt.js +4 -1
- package/dist/system-prompt.js.map +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.js +1 -1
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.js +1 -1
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.js +1 -1
- package/dist/tools/fundamentals/earnings.js.map +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 +1 -0
- 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 +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.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.js +29 -5
- package/dist/tools/macro/fred-data.js.map +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.js +1 -1
- package/dist/tools/market/crypto-price.js.map +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.js +1 -1
- package/dist/tools/market/stock-history.js.map +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.js +9 -4
- package/dist/tools/options/option-chain.js.map +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.js +1 -1
- package/dist/tools/portfolio/predictions.js.map +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.js +1 -1
- package/dist/tools/portfolio/tracker.js.map +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.js +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js.map +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/twitter-sentiment.js +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js.map +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.js +1 -1
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +2 -2
- package/dist/tools/technical/backtest.js +41 -27
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -3
- 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 +88 -7
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +7 -3
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/ask-user-bridge.ts +82 -0
- package/gui/server/gui-session-manager.ts +5 -0
- package/gui/server/projector.ts +47 -5
- package/gui/server/prompt-observation.ts +61 -0
- package/gui/server/server.ts +119 -8
- package/gui/server/session-entry-wait.ts +81 -0
- package/gui/web/dist/assets/{CatalogOverlay-D1ImSJTe.js → CatalogOverlay-Bmp6Knu7.js} +1 -1
- package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
- package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +18 -12
- package/src/cli.ts +2 -1
- package/src/config.ts +89 -5
- package/src/infra/browser.ts +1 -1
- package/src/infra/rate-limiter.ts +10 -1
- package/src/memory/manager.ts +43 -10
- package/src/memory/storage.ts +3 -2
- package/src/memory/types.ts +4 -0
- package/src/pi/opencandle-extension.ts +273 -42
- package/src/pi/setup.ts +10 -0
- package/src/prompts/context-builder.ts +128 -17
- package/src/prompts/disclaimer.ts +1 -1
- package/src/prompts/policy-cards.ts +220 -0
- package/src/prompts/sections.ts +3 -3
- package/src/prompts/workflow-prompts.ts +172 -18
- package/src/providers/alpha-vantage.ts +24 -1
- package/src/providers/sec-edgar.ts +220 -4
- package/src/providers/web-search.ts +1 -1
- package/src/providers/yahoo-finance.ts +171 -4
- package/src/routing/classify-intent.ts +94 -3
- package/src/routing/defaults.ts +3 -3
- package/src/routing/entity-extractor.ts +164 -13
- package/src/routing/index.ts +44 -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-prompt.ts +50 -46
- package/src/routing/router-types.ts +21 -0
- package/src/routing/router.ts +511 -12
- package/src/routing/slot-resolver.ts +44 -6
- package/src/routing/turn-context.ts +111 -0
- package/src/routing/types.ts +13 -1
- package/src/runtime/answer-contracts.ts +633 -0
- package/src/runtime/artifact-contracts.ts +76 -0
- package/src/runtime/planning-evidence.ts +591 -0
- package/src/runtime/session-coordinator.ts +78 -12
- package/src/system-prompt.ts +4 -1
- package/src/tools/fundamentals/company-overview.ts +1 -1
- package/src/tools/fundamentals/comps.ts +1 -1
- package/src/tools/fundamentals/dcf.ts +1 -1
- package/src/tools/fundamentals/earnings.ts +1 -1
- package/src/tools/fundamentals/financials.ts +1 -1
- package/src/tools/fundamentals/sec-filings.ts +25 -2
- package/src/tools/index.ts +3 -0
- package/src/tools/macro/fear-greed.ts +1 -1
- package/src/tools/macro/fred-data.ts +31 -5
- package/src/tools/market/crypto-history.ts +18 -2
- package/src/tools/market/crypto-price.ts +1 -1
- package/src/tools/market/search-ticker.ts +1 -1
- package/src/tools/market/stock-history.ts +1 -1
- package/src/tools/market/stock-quote.ts +1 -1
- package/src/tools/options/greeks.ts +0 -1
- package/src/tools/options/option-chain.ts +9 -4
- package/src/tools/portfolio/correlation.ts +1 -1
- package/src/tools/portfolio/holdings-overlap.ts +123 -0
- package/src/tools/portfolio/predictions.ts +1 -1
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/portfolio/tracker.ts +1 -1
- package/src/tools/portfolio/watchlist.ts +10 -4
- package/src/tools/sentiment/reddit-sentiment.ts +1 -1
- package/src/tools/sentiment/sentiment-summary.ts +62 -2
- package/src/tools/sentiment/twitter-sentiment.ts +1 -1
- package/src/tools/sentiment/web-search.ts +36 -3
- package/src/tools/sentiment/web-sentiment.ts +1 -1
- package/src/tools/technical/backtest.ts +50 -29
- package/src/tools/technical/indicators.ts +1 -3
- package/src/types/options.ts +17 -0
- package/src/types/portfolio.ts +32 -0
- package/src/workflows/compare-assets.ts +38 -2
- package/src/workflows/options-screener.ts +85 -7
- package/src/workflows/portfolio-builder.ts +7 -3
- 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/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
|
@@ -3,10 +3,12 @@ import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
|
|
|
3
3
|
import { rateLimiter } from "../infra/rate-limiter.js";
|
|
4
4
|
import { StealthBrowser } from "../infra/browser.js";
|
|
5
5
|
import type { StockQuote, OHLCV } from "../types/market.js";
|
|
6
|
-
import type { OptionsChain, OptionContract } from "../types/options.js";
|
|
6
|
+
import type { OptionsChain, OptionContract, OptionsMarketSession, OptionsQuoteStatus } from "../types/options.js";
|
|
7
|
+
import type { FundHoldings } from "../types/portfolio.js";
|
|
7
8
|
import { computeGreeks } from "../tools/options/greeks.js";
|
|
8
9
|
|
|
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";
|
|
10
12
|
|
|
11
13
|
interface YahooChartResponse {
|
|
12
14
|
chart: {
|
|
@@ -28,6 +30,29 @@ interface YahooChartResponse {
|
|
|
28
30
|
};
|
|
29
31
|
}
|
|
30
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
|
+
|
|
31
56
|
export async function getQuote(symbol: string): Promise<StockQuote> {
|
|
32
57
|
const cacheKey = `yahoo:quote:${symbol}`;
|
|
33
58
|
const cached = cache.get<StockQuote>(cacheKey);
|
|
@@ -128,6 +153,80 @@ export async function getHistory(
|
|
|
128
153
|
}
|
|
129
154
|
}
|
|
130
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
|
+
|
|
131
230
|
// --- Options Chain (v7 API with crumb+cookie auth) ---
|
|
132
231
|
|
|
133
232
|
const BROWSER_UA =
|
|
@@ -227,7 +326,7 @@ export async function getOptionsChain(
|
|
|
227
326
|
try {
|
|
228
327
|
const browserData = await fetchOptionsViaBrowser(symbol, expiration);
|
|
229
328
|
if (browserData) {
|
|
230
|
-
const chain = parseOptionsResponse(
|
|
329
|
+
const chain = parseOptionsResponse(browserData);
|
|
231
330
|
cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
|
|
232
331
|
return chain;
|
|
233
332
|
}
|
|
@@ -252,7 +351,7 @@ export async function getOptionsChain(
|
|
|
252
351
|
}
|
|
253
352
|
|
|
254
353
|
const data: YahooOptionsResponse = await res.json();
|
|
255
|
-
const chain = parseOptionsResponse(
|
|
354
|
+
const chain = parseOptionsResponse(data);
|
|
256
355
|
cache.set(cacheKey, chain, TTL.OPTIONS_CHAIN);
|
|
257
356
|
return chain;
|
|
258
357
|
}
|
|
@@ -275,7 +374,73 @@ export function computeTimeToExpiry(expirationTs: number, nowMs: number = Date.n
|
|
|
275
374
|
return Math.max(MIN_TIME_YEARS, remainingS / SECONDS_PER_YEAR);
|
|
276
375
|
}
|
|
277
376
|
|
|
278
|
-
function
|
|
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 {
|
|
279
444
|
if (data.optionChain.error) {
|
|
280
445
|
throw new Error(`Yahoo Finance options: ${JSON.stringify(data.optionChain.error)}`);
|
|
281
446
|
}
|
|
@@ -314,6 +479,7 @@ function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): Optio
|
|
|
314
479
|
const puts = (opts.puts ?? []).map((c: any) => mapContract(c, "put"));
|
|
315
480
|
const totalCallVolume = calls.reduce((s, c) => s + c.volume, 0);
|
|
316
481
|
const totalPutVolume = puts.reduce((s, c) => s + c.volume, 0);
|
|
482
|
+
const quoteStatus = buildOptionsQuoteStatus([...calls, ...puts]);
|
|
317
483
|
|
|
318
484
|
return {
|
|
319
485
|
symbol: result.underlyingSymbol,
|
|
@@ -325,6 +491,7 @@ function parseOptionsResponse(symbol: string, data: YahooOptionsResponse): Optio
|
|
|
325
491
|
totalCallVolume,
|
|
326
492
|
totalPutVolume,
|
|
327
493
|
putCallRatio: totalCallVolume > 0 ? totalPutVolume / totalCallVolume : 0,
|
|
494
|
+
quoteStatus,
|
|
328
495
|
fetchedAt: new Date().toISOString(),
|
|
329
496
|
};
|
|
330
497
|
}
|
|
@@ -29,7 +29,21 @@ 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) || /\bbiggest\s+risk\b/.test(lower) || /\bconcentration\b/.test(lower))
|
|
33
47
|
);
|
|
34
48
|
},
|
|
35
49
|
},
|
|
@@ -56,6 +70,67 @@ const RULES: Rule[] = [
|
|
|
56
70
|
return hasNewsKeyword;
|
|
57
71
|
},
|
|
58
72
|
},
|
|
73
|
+
// Tool-backed finance tasks that are not a structured multi-step workflow.
|
|
74
|
+
{
|
|
75
|
+
workflow: "general_finance_qa",
|
|
76
|
+
confidence: 0.9,
|
|
77
|
+
test: (input, entities) => {
|
|
78
|
+
const lower = input.toLowerCase();
|
|
79
|
+
const hasOptionKeywords =
|
|
80
|
+
/\bcalls?\b/.test(lower) ||
|
|
81
|
+
/\bputs?\b/.test(lower) ||
|
|
82
|
+
/\boption(?:s)?\s*chain\b/.test(lower) ||
|
|
83
|
+
/\boptions?\b/.test(lower);
|
|
84
|
+
const hasCompareKeywords =
|
|
85
|
+
/\bcompare\b/.test(lower) ||
|
|
86
|
+
/\bvs\.?\b/.test(lower) ||
|
|
87
|
+
/\bversus\b/.test(lower) ||
|
|
88
|
+
/\bwhich\s+is\s+better\b/.test(lower);
|
|
89
|
+
|
|
90
|
+
if (hasOptionKeywords && entities.symbols.length >= 1) return false;
|
|
91
|
+
if (hasCompareKeywords && entities.symbols.length >= 2) return false;
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
/\bbacktest\b/.test(lower) ||
|
|
95
|
+
/\bsentiment\b/.test(lower) ||
|
|
96
|
+
/\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",
|
|
@@ -88,9 +163,10 @@ const RULES: Rule[] = [
|
|
|
88
163
|
{
|
|
89
164
|
workflow: "compare_assets",
|
|
90
165
|
confidence: 0.85,
|
|
91
|
-
test: (input) => {
|
|
166
|
+
test: (input, entities) => {
|
|
92
167
|
const lower = input.toLowerCase();
|
|
93
|
-
return
|
|
168
|
+
return entities.symbols.length >= 2 &&
|
|
169
|
+
/\bcompare\s+[a-z]{1,5}\b(?:\s*,?\s*(?:and\s+)?[a-z]{1,5}\b)+/.test(lower);
|
|
94
170
|
},
|
|
95
171
|
},
|
|
96
172
|
// Compare: 2+ uppercase symbols without explicit keyword
|
|
@@ -161,6 +237,9 @@ const RULES: Rule[] = [
|
|
|
161
237
|
},
|
|
162
238
|
];
|
|
163
239
|
|
|
240
|
+
/**
|
|
241
|
+
* @deprecated Use the LLM router (`route`) for new classification paths; keep this only for rules-mode fallback and deterministic safety nets.
|
|
242
|
+
*/
|
|
164
243
|
export function classifyIntent(input: string): ClassificationResult {
|
|
165
244
|
const trimmed = input.trim();
|
|
166
245
|
if (!trimmed) {
|
|
@@ -192,3 +271,15 @@ export function classifyIntent(input: string): ClassificationResult {
|
|
|
192
271
|
entities,
|
|
193
272
|
};
|
|
194
273
|
}
|
|
274
|
+
|
|
275
|
+
function isPortfolioEvaluationRequest(input: string): boolean {
|
|
276
|
+
const lower = input.toLowerCase();
|
|
277
|
+
const hasEvaluationIntent =
|
|
278
|
+
/\b(?:evaluat(?:e|ion)|review|assess|analy[sz]e|prospects?|risks?|opportunities?|mitigat(?:e|ion)|adjustment)\b/.test(lower);
|
|
279
|
+
const hasPortfolioObject =
|
|
280
|
+
/\b(?:portfolio|allocation|asset\s+allocation|60\/40|equity|fixed\s+income|bonds?)\b/.test(lower);
|
|
281
|
+
const hasConstructionIntent =
|
|
282
|
+
/\b(?:build|create|construct|put\s+together|invest|allocate)\b/.test(lower) &&
|
|
283
|
+
(/\$\s*\d|\b\d+(?:\.\d+)?\s*k\b|\bbudget\b|\bcapital\b/.test(lower));
|
|
284
|
+
return hasEvaluationIntent && hasPortfolioObject && !hasConstructionIntent;
|
|
285
|
+
}
|
package/src/routing/defaults.ts
CHANGED
|
@@ -3,9 +3,9 @@ import type { PortfolioSlots, OptionsScreenerSlots } from "./types.js";
|
|
|
3
3
|
export const PORTFOLIO_DEFAULTS: Omit<PortfolioSlots, "budget"> = {
|
|
4
4
|
riskProfile: "balanced",
|
|
5
5
|
timeHorizon: "1y_plus",
|
|
6
|
-
assetScope: "
|
|
7
|
-
positionCount:
|
|
8
|
-
maxSinglePositionPct:
|
|
6
|
+
assetScope: "diversified_etf_building_blocks",
|
|
7
|
+
positionCount: 6,
|
|
8
|
+
maxSinglePositionPct: 20,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
export const OPTIONS_SCREENER_DEFAULTS: Omit<OptionsScreenerSlots, "symbol" | "direction"> = {
|
|
@@ -7,11 +7,13 @@ const COMMON_WORDS = new Set([
|
|
|
7
7
|
"HIM", "HIS", "HOW", "ITS", "LET", "MAY", "NEW", "NOW", "OLD", "OUR", "OWN",
|
|
8
8
|
"SAY", "SHE", "TOO", "USE", "WAY", "WHO", "BOY", "DID", "GET", "HAS", "HIM",
|
|
9
9
|
"OUT", "PUT", "RUN", "SET", "TOP", "WHY", "BIG", "END", "FAR", "FEW",
|
|
10
|
-
"GOT", "LOW", "MAN", "OFF", "PAY", "TRY", "TWO", "BUY", "ETF", "ETFS",
|
|
10
|
+
"GOT", "LOW", "MAN", "OFF", "PAY", "TRY", "TWO", "BUY", "DOES", "ETF", "ETFS",
|
|
11
11
|
// Technical analysis acronyms
|
|
12
12
|
"SMA", "EMA", "RSI", "MACD", "OBV", "ATR", "ADX", "VWAP",
|
|
13
13
|
// Fundamental analysis acronyms
|
|
14
14
|
"DCF", "FCF", "ROE", "ROA", "ROI", "EPS", "NAV", "WACC", "EBIT",
|
|
15
|
+
// Regulatory / source acronyms that are not tickers in natural language
|
|
16
|
+
"SEC",
|
|
15
17
|
"BEST", "WHAT", "WITH", "THAT", "THIS", "FROM", "HAVE", "BEEN", "SOME",
|
|
16
18
|
"THEM", "THAN", "LIKE", "JUST", "OVER", "ALSO", "BACK", "MUCH", "MOST",
|
|
17
19
|
"ONLY", "VERY", "WHEN", "COME", "MAKE", "FIND", "HERE", "KNOW", "TAKE",
|
|
@@ -19,22 +21,50 @@ const COMMON_WORDS = new Set([
|
|
|
19
21
|
"NEXT", "SHOW", "LAST",
|
|
20
22
|
]);
|
|
21
23
|
|
|
24
|
+
const AMBIGUOUS_CONCEPT_TICKERS = new Set(["AI", "CPI", "FRED", "GUI"]);
|
|
25
|
+
const LOWERCASE_FINANCE_TERMS = new Set([
|
|
26
|
+
"bond", "bonds", "cash", "rate", "rates", "cuts", "gold", "oil", "stock", "stocks",
|
|
27
|
+
"fund", "funds", "etf", "etfs", "puts", "calls", "option", "options",
|
|
28
|
+
]);
|
|
29
|
+
|
|
22
30
|
export function extractEntities(input: string): ExtractedEntities {
|
|
31
|
+
const symbols = extractSymbols(input);
|
|
32
|
+
const heldSymbol = extractHeldSymbol(input, symbols);
|
|
33
|
+
const catalystSymbols = heldSymbol
|
|
34
|
+
? symbols.filter((symbol) => symbol !== heldSymbol)
|
|
35
|
+
: [];
|
|
23
36
|
return {
|
|
24
|
-
symbols
|
|
37
|
+
symbols,
|
|
25
38
|
budget: extractBudget(input),
|
|
26
39
|
maxPremium: extractMaxPremium(input),
|
|
40
|
+
costBasis: extractCostBasis(input),
|
|
41
|
+
shareQuantity: extractShareQuantity(input),
|
|
27
42
|
direction: extractDirection(input),
|
|
28
43
|
riskProfile: extractRiskProfile(input),
|
|
29
44
|
dteHint: extractDteHint(input),
|
|
45
|
+
optionStrategy: extractOptionStrategy(input),
|
|
46
|
+
heldSymbol,
|
|
47
|
+
catalystSymbols: catalystSymbols.length > 0 ? catalystSymbols : undefined,
|
|
30
48
|
timeHorizon: extractTimeHorizon(input),
|
|
49
|
+
assetScope: extractAssetScope(input),
|
|
50
|
+
compareMetrics: extractCompareMetrics(input),
|
|
31
51
|
};
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
export function extractBudget(input: string): number | undefined {
|
|
55
|
+
if (
|
|
56
|
+
/\b(?:at|above|below|under|over|near)\s+\$\s*[\d,]+(?:\.\d+)?\s*([kK])?\b/i.test(input) &&
|
|
57
|
+
!hasBudgetContext(input)
|
|
58
|
+
) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
// Match $10,000 or $10000 or $10k
|
|
36
63
|
const dollarSign = input.match(/\$\s*([\d,]+(?:\.\d+)?)\s*([kK])?\b/);
|
|
37
64
|
if (dollarSign) {
|
|
65
|
+
if (isNonBudgetDollarAmount(input, dollarSign.index ?? 0, dollarSign[0].length)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
38
68
|
const base = parseFloat(dollarSign[1].replace(/,/g, ""));
|
|
39
69
|
return dollarSign[2] ? base * 1000 : base;
|
|
40
70
|
}
|
|
@@ -54,34 +84,88 @@ export function extractBudget(input: string): number | undefined {
|
|
|
54
84
|
return undefined;
|
|
55
85
|
}
|
|
56
86
|
|
|
87
|
+
function hasBudgetContext(input: string): boolean {
|
|
88
|
+
return /\b(?:budget|invest|allocate|portfolio|cash|capital|with|have|spend|put\s+to\s+work)\b/i.test(input);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isNonBudgetDollarAmount(input: string, start: number, length: number): boolean {
|
|
92
|
+
const before = input.slice(Math.max(0, start - 32), start);
|
|
93
|
+
const after = input.slice(start + length, start + length + 24);
|
|
94
|
+
return /\b(?:cost\s*basis|basis|entry(?:\s*price)?)\s*(?:is|at|of|:)?\s*$/i.test(before) ||
|
|
95
|
+
/^\s*(?:premium|max\s+premium|cost\s*basis|basis|entry(?:\s*price)?)\b/i.test(after);
|
|
96
|
+
}
|
|
97
|
+
|
|
57
98
|
function extractSymbols(input: string): string[] {
|
|
58
99
|
const symbols: string[] = [];
|
|
100
|
+
const addSymbol = (raw: string | undefined, options: { lowercaseContext?: boolean } = {}) => {
|
|
101
|
+
const symbol = raw?.toUpperCase();
|
|
102
|
+
if (options.lowercaseContext && LOWERCASE_FINANCE_TERMS.has(String(raw || "").toLowerCase())) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
symbol &&
|
|
107
|
+
symbol.length >= 1 &&
|
|
108
|
+
symbol.length <= 5 &&
|
|
109
|
+
/^[A-Z]+$/.test(symbol) &&
|
|
110
|
+
!COMMON_WORDS.has(symbol) &&
|
|
111
|
+
!isAmbiguousConceptUsage(input, symbol) &&
|
|
112
|
+
!symbols.includes(symbol)
|
|
113
|
+
) {
|
|
114
|
+
symbols.push(symbol);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
59
117
|
|
|
60
118
|
// Match $TICKER patterns
|
|
61
119
|
const dollarTickers = input.matchAll(/\$([A-Za-z]{1,5})\b/g);
|
|
62
120
|
for (const match of dollarTickers) {
|
|
63
|
-
|
|
121
|
+
addSymbol(match[1]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Match explicit lowercase ticker contexts without treating arbitrary short
|
|
125
|
+
// words as symbols.
|
|
126
|
+
const lowercaseCompare = input.match(/\bcompare\s+([a-z]{1,5})\s+(?:and|vs\.?|versus)\s+([a-z]{1,5})\b/i);
|
|
127
|
+
if (lowercaseCompare) {
|
|
128
|
+
addSymbol(lowercaseCompare[1], { lowercaseContext: true });
|
|
129
|
+
addSymbol(lowercaseCompare[2], { lowercaseContext: true });
|
|
130
|
+
}
|
|
131
|
+
const lowercaseTickerContext = input.matchAll(/\b(?:analy[sz]e|quote|ticker)\s+\$?([a-z]{1,5})\b|\b\$?([a-z]{1,5})\s+(?:ticker|stock|shares?|quote|options?|calls?|puts?)\b/gi);
|
|
132
|
+
for (const match of lowercaseTickerContext) {
|
|
133
|
+
addSymbol(match[1] ?? match[2], { lowercaseContext: true });
|
|
134
|
+
}
|
|
135
|
+
const lowercaseHeldPosition = input.matchAll(/\b(?:own|hold|holding|long|protect|hedge|have)\s+\d+(?:,\d{3})*\s+shares?\s+(?:of\s+)?\$?([a-z]{1,5})\b|\b\d+(?:,\d{3})*\s+shares?\s+of\s+\$?([a-z]{1,5})\b/gi);
|
|
136
|
+
for (const match of lowercaseHeldPosition) {
|
|
137
|
+
const raw = match[1] ?? match[2];
|
|
138
|
+
if (raw && raw !== raw.toUpperCase()) {
|
|
139
|
+
addSymbol(raw, { lowercaseContext: true });
|
|
140
|
+
}
|
|
64
141
|
}
|
|
65
142
|
|
|
66
143
|
// Match standalone uppercase tickers (1-5 chars, all caps)
|
|
67
144
|
const words = input.split(/[\s,]+/);
|
|
68
145
|
for (const word of words) {
|
|
69
146
|
const cleaned = word.replace(/[^A-Za-z]/g, "");
|
|
70
|
-
if (
|
|
71
|
-
cleaned
|
|
72
|
-
cleaned.length <= 5 &&
|
|
73
|
-
cleaned === cleaned.toUpperCase() &&
|
|
74
|
-
/^[A-Z]+$/.test(cleaned) &&
|
|
75
|
-
!COMMON_WORDS.has(cleaned) &&
|
|
76
|
-
!symbols.includes(cleaned)
|
|
77
|
-
) {
|
|
78
|
-
symbols.push(cleaned);
|
|
147
|
+
if (cleaned === cleaned.toUpperCase()) {
|
|
148
|
+
addSymbol(cleaned);
|
|
79
149
|
}
|
|
80
150
|
}
|
|
81
151
|
|
|
82
152
|
return symbols;
|
|
83
153
|
}
|
|
84
154
|
|
|
155
|
+
export function isAmbiguousConceptUsage(input: string, symbol: string): boolean {
|
|
156
|
+
if (!AMBIGUOUS_CONCEPT_TICKERS.has(symbol)) return false;
|
|
157
|
+
if (new RegExp(`\\$${symbol}\\b`).test(input)) return false;
|
|
158
|
+
if (
|
|
159
|
+
new RegExp(
|
|
160
|
+
`\\b(?:analyze|quote|ticker|stock|shares?|options?|calls?|puts?)\\s+${symbol}\\b|\\b${symbol}\\s+(?:ticker|stock|shares?|quote|options?|calls?|puts?)\\b`,
|
|
161
|
+
"i",
|
|
162
|
+
).test(input)
|
|
163
|
+
) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
85
169
|
function extractMaxPremium(input: string): number | undefined {
|
|
86
170
|
const lower = input.toLowerCase();
|
|
87
171
|
if (!/\bpremium\b/.test(lower)) return undefined;
|
|
@@ -103,6 +187,36 @@ function extractMaxPremium(input: string): number | undefined {
|
|
|
103
187
|
return undefined;
|
|
104
188
|
}
|
|
105
189
|
|
|
190
|
+
function extractHeldSymbol(input: string, symbols: string[]): string | undefined {
|
|
191
|
+
const patterns = [
|
|
192
|
+
/\b(?:i\s+)?(?:have|own|hold|holding|long)\s+\d+(?:,\d{3})*\s+shares?\s+(?:of\s+)?\$?([A-Za-z]{1,5})\b/i,
|
|
193
|
+
/\b(?:my\s+)?\d+(?:,\d{3})*\s+\$?([A-Za-z]{1,5})\s+shares?\b/i,
|
|
194
|
+
/\b(?:i\s+)?(?:have|own|hold|holding|long)\s+\$?([A-Za-z]{1,5})\b/i,
|
|
195
|
+
/\bmy\s+\$?([A-Za-z]{1,5})\s+(?:position|shares?|stock)\b/i,
|
|
196
|
+
];
|
|
197
|
+
for (const pattern of patterns) {
|
|
198
|
+
const match = input.match(pattern);
|
|
199
|
+
const symbol = match?.[1]?.toUpperCase();
|
|
200
|
+
if (symbol && symbols.includes(symbol)) return symbol;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function extractShareQuantity(input: string): number | undefined {
|
|
206
|
+
const match = input.match(/\b(\d{1,3}(?:,\d{3})*|\d{1,6})\s+shares?\b/i);
|
|
207
|
+
if (!match) return undefined;
|
|
208
|
+
const value = parseInt(match[1].replace(/,/g, ""), 10);
|
|
209
|
+
return Number.isFinite(value) ? value : undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function extractCostBasis(input: string): number | undefined {
|
|
213
|
+
const match = input.match(/\b(?:cost\s*basis|basis|entry(?:\s*price)?)\s*(?:is|at|of|:)?\s*\$?\s*([\d,]+(?:\.\d+)?)\b/i) ??
|
|
214
|
+
input.match(/\$\s*([\d,]+(?:\.\d+)?)\s*(?:cost\s*basis|basis|entry(?:\s*price)?)\b/i);
|
|
215
|
+
if (!match) return undefined;
|
|
216
|
+
const value = parseFloat(match[1].replace(/,/g, ""));
|
|
217
|
+
return Number.isFinite(value) ? value : undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
106
220
|
function extractDirection(input: string): "bullish" | "bearish" | undefined {
|
|
107
221
|
const lower = input.toLowerCase();
|
|
108
222
|
if (/\bcalls?\b/.test(lower) || /\bbullish\b/.test(lower)) return "bullish";
|
|
@@ -126,15 +240,52 @@ function extractRiskProfile(input: string): string | undefined {
|
|
|
126
240
|
|
|
127
241
|
function extractDteHint(input: string): string | undefined {
|
|
128
242
|
const lower = input.toLowerCase();
|
|
129
|
-
|
|
243
|
+
const explicitDays = lower.match(/\b(\d+)\s*(?:-|to|or)\s*(\d+)\s*(?:dte|days?)\b/);
|
|
244
|
+
if (explicitDays) return `${explicitDays[1]}-${explicitDays[2]} days`;
|
|
130
245
|
if (/\bmonth\b/.test(lower)) return "month";
|
|
246
|
+
if (/\bearnings?\b.*\b(?:today|tonight|this\s+week)\b|\b(?:today|tonight|this\s+week)\b.*\bearnings?\b/.test(lower)) return "event_week";
|
|
247
|
+
if (/\bleaps?\b/i.test(lower) || /\blong[\s-]*dated\b/.test(lower)) return "leaps";
|
|
131
248
|
if (/\bweek(?:ly|s?)?\b/.test(lower)) return "week";
|
|
132
249
|
return undefined;
|
|
133
250
|
}
|
|
134
251
|
|
|
252
|
+
function extractOptionStrategy(input: string): ExtractedEntities["optionStrategy"] | undefined {
|
|
253
|
+
const lower = input.toLowerCase();
|
|
254
|
+
if (/\bcovered\s+calls?\b/.test(lower)) return "covered_call";
|
|
255
|
+
if (/\b(?:protective|married)\s+puts?\b/.test(lower)) return "protective_put";
|
|
256
|
+
if (
|
|
257
|
+
/\b(?:hedge|protect|protection|insurance)\b/.test(lower) &&
|
|
258
|
+
/\bputs?\b/.test(lower) &&
|
|
259
|
+
/\b(?:own|hold|holding|long|shares?|position)\b/.test(lower)
|
|
260
|
+
) {
|
|
261
|
+
return "protective_put";
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
135
266
|
function extractTimeHorizon(input: string): string | undefined {
|
|
136
267
|
const lower = input.toLowerCase();
|
|
268
|
+
const explicitMonths = lower.match(/\b(\d+)\s*(?:month|months|mo|mos)\b/);
|
|
269
|
+
if (explicitMonths) return `${explicitMonths[1]}mo`;
|
|
270
|
+
const explicitYears = lower.match(/\b(\d+)\s*(?:year|years|yr|yrs)\b/);
|
|
271
|
+
if (explicitYears) return `${explicitYears[1]}_years`;
|
|
137
272
|
if (/\bshort[\s-]*term\b/.test(lower) || /\bday[\s-]*trad/i.test(lower)) return "short";
|
|
138
273
|
if (/\blong[\s-]*term\b/.test(lower) || /\bbuy[\s-]*and[\s-]*hold\b/.test(lower)) return "long";
|
|
139
274
|
return undefined;
|
|
140
275
|
}
|
|
276
|
+
|
|
277
|
+
function extractAssetScope(input: string): string | undefined {
|
|
278
|
+
const lower = input.toLowerCase();
|
|
279
|
+
if (/\betfs?\b/.test(lower)) return "etf_focused";
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function extractCompareMetrics(input: string): string[] | undefined {
|
|
284
|
+
const lower = input.toLowerCase();
|
|
285
|
+
const metrics: string[] = [];
|
|
286
|
+
if (/\bsentiment\b/.test(lower)) metrics.push("sentiment");
|
|
287
|
+
if (/\b(?:macro\s*)?hedg(?:e|ing)\b/.test(lower)) metrics.push("macro_hedge");
|
|
288
|
+
if (/\b(?:rates?|rate\s*cuts?|fed|federal\s+funds?|interest\s+rates?)\b/.test(lower)) metrics.push("interest_rates");
|
|
289
|
+
if (/\b(?:overlap|same\s+stuff|same\s+holdings|concentration|too\s+much\s+of\s+the\s+same)\b/.test(lower)) metrics.push("overlap");
|
|
290
|
+
return metrics.length > 0 ? metrics : undefined;
|
|
291
|
+
}
|