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
|
@@ -48,7 +48,11 @@ const DISPLAY_NAMES: Record<string, string> = {
|
|
|
48
48
|
objective: "objective",
|
|
49
49
|
moneynessPreference: "moneyness",
|
|
50
50
|
liquidityMinimum: "liquidity",
|
|
51
|
+
optionStrategy: "option strategy",
|
|
52
|
+
costBasis: "cost basis",
|
|
53
|
+
shareQuantity: "share quantity",
|
|
51
54
|
symbols: "symbols",
|
|
55
|
+
metrics: "metrics",
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
/**
|
|
@@ -62,6 +66,8 @@ export function buildDisclosureBlock(
|
|
|
62
66
|
): string {
|
|
63
67
|
const userSpecified: string[] = [];
|
|
64
68
|
const fromPreferences: string[] = [];
|
|
69
|
+
const fromPriorContext: string[] = [];
|
|
70
|
+
const fromMemory: string[] = [];
|
|
65
71
|
const defaults: string[] = [];
|
|
66
72
|
|
|
67
73
|
for (const [key, source] of Object.entries(slotSources)) {
|
|
@@ -75,6 +81,12 @@ export function buildDisclosureBlock(
|
|
|
75
81
|
case "preference":
|
|
76
82
|
fromPreferences.push(display);
|
|
77
83
|
break;
|
|
84
|
+
case "prior_context":
|
|
85
|
+
fromPriorContext.push(display);
|
|
86
|
+
break;
|
|
87
|
+
case "memory":
|
|
88
|
+
fromMemory.push(display);
|
|
89
|
+
break;
|
|
78
90
|
case "default":
|
|
79
91
|
defaults.push(display);
|
|
80
92
|
break;
|
|
@@ -85,6 +97,8 @@ export function buildDisclosureBlock(
|
|
|
85
97
|
lines.push("Assumptions (reproduce this block exactly — do not relabel sources):");
|
|
86
98
|
if (userSpecified.length > 0) lines.push(` User-specified: ${userSpecified.join(", ")}`);
|
|
87
99
|
if (fromPreferences.length > 0) lines.push(` From saved preferences: ${fromPreferences.join(", ")}`);
|
|
100
|
+
if (fromPriorContext.length > 0) lines.push(` From prior context: ${fromPriorContext.join(", ")}`);
|
|
101
|
+
if (fromMemory.length > 0) lines.push(` From memory: ${fromMemory.join(", ")}`);
|
|
88
102
|
if (defaults.length > 0) lines.push(` Defaults: ${defaults.join(", ")}`);
|
|
89
103
|
if (workflowConstraints && workflowConstraints.length > 0) {
|
|
90
104
|
lines.push(` Workflow constraints: ${workflowConstraints.join(", ")}`);
|
|
@@ -124,7 +138,8 @@ function formatSlotValue(value: unknown): string {
|
|
|
124
138
|
|
|
125
139
|
export function buildPortfolioPrompt(resolution: SlotResolution<PortfolioSlots>): string {
|
|
126
140
|
const { resolved: s, sources } = resolution;
|
|
127
|
-
const
|
|
141
|
+
const normalizedScope = s.assetScope.toLowerCase();
|
|
142
|
+
const isFundBuildingBlocks = normalizedScope.includes("etf") || normalizedScope.includes("fund") || normalizedScope.includes("building_blocks");
|
|
128
143
|
|
|
129
144
|
const disclosureBlock = buildDisclosureBlock(
|
|
130
145
|
{
|
|
@@ -138,12 +153,14 @@ export function buildPortfolioPrompt(resolution: SlotResolution<PortfolioSlots>)
|
|
|
138
153
|
sources as Record<string, SlotSource | undefined>,
|
|
139
154
|
);
|
|
140
155
|
|
|
141
|
-
const toolSteps =
|
|
142
|
-
? `1. Identify ${s.positionCount}
|
|
156
|
+
const toolSteps = isFundBuildingBlocks
|
|
157
|
+
? `1. Identify ${s.positionCount} diversified fund/ETF building-block candidates appropriate for a ${s.riskProfile} ${s.timeHorizon} portfolio.
|
|
158
|
+
Include distinct asset-class roles such as core domestic equity, international equity, fixed income, short-duration or cash-like stability, and inflation-sensitive ballast when appropriate.
|
|
143
159
|
2. Use get_stock_quote for each candidate to get current prices.
|
|
144
160
|
3. Use analyze_risk on each candidate for volatility, Sharpe, and max drawdown.
|
|
145
161
|
4. Use analyze_correlation across all candidates to check diversification.`
|
|
146
162
|
: `1. Identify ${s.positionCount} diverse candidates appropriate for a ${s.riskProfile} ${s.timeHorizon} portfolio.
|
|
163
|
+
Avoid over-concentration in individual equities unless the user explicitly asked for stock picks; use diversified funds where they better fit the requested horizon and risk profile.
|
|
147
164
|
2. Use get_stock_quote for each candidate to get current prices.
|
|
148
165
|
3. Use get_company_overview for fundamentals on each candidate.
|
|
149
166
|
4. Use analyze_risk on each candidate for volatility, Sharpe, and max drawdown.
|
|
@@ -162,13 +179,22 @@ Build a draft portfolio under these parameters:
|
|
|
162
179
|
Steps:
|
|
163
180
|
${toolSteps}
|
|
164
181
|
|
|
182
|
+
Portfolio construction guardrails:
|
|
183
|
+
- For broad balanced portfolio requests, prefer diversified building blocks over individual-company concentration unless the user explicitly asks for stocks.
|
|
184
|
+
- For horizons under 5 years, include enough fixed-income, short-duration, cash-like, or inflation-sensitive ballast to make the drawdown risk match the horizon.
|
|
185
|
+
- If a candidate's risk metrics undermine its role (for example materially negative risk-adjusted returns, high drawdown, or excessive correlation), lower the allocation, name a role-equivalent replacement, or explain why you are keeping it.
|
|
186
|
+
- Keep rationale tied to each holding's role in this portfolio; do not paste company descriptions or generic issuer background.
|
|
187
|
+
|
|
165
188
|
${disclosureBlock}
|
|
166
189
|
|
|
167
190
|
Response format:
|
|
168
191
|
- Start with the assumptions block above exactly as written. Do not relabel source attribution anywhere else in your response.
|
|
192
|
+
- Then start the analysis with "Bottom line:" and directly say what portfolio you would build for the user.
|
|
169
193
|
- Commit to the draft: give concrete percentages for each position, not ranges, and not "consider allocating X-Y%".
|
|
170
|
-
- Present an allocation table: symbol, allocation %, dollar amount, and a one-line analyst rationale for each position (what the data showed).
|
|
194
|
+
- Present an allocation table: symbol, allocation %, dollar amount, current price used, estimated shares, role, and a one-line analyst rationale for each position (what the data showed and why it belongs in this portfolio).
|
|
195
|
+
- After the table, add a brief "Why this fits the horizon" summary explaining the growth/stability tradeoff and horizon-specific risks for the stated time horizon.
|
|
171
196
|
- Include a risk summary (portfolio volatility, diversification quality) and an invalidation condition for the overall draft ("revisit if correlation exceeds 0.7 across the core ETFs" or equivalent).
|
|
197
|
+
- Include practical implementation notes: rebalance cadence, low-cost/liquid implementation, and tax/account caveats where relevant.
|
|
172
198
|
- Suggest what to change for more growth or more safety.`;
|
|
173
199
|
}
|
|
174
200
|
|
|
@@ -216,11 +242,62 @@ For LEAPS / long-dated options:
|
|
|
216
242
|
objective: s.objective,
|
|
217
243
|
moneynessPreference: s.moneynessPreference,
|
|
218
244
|
liquidityMinimum: s.liquidityMinimum,
|
|
245
|
+
...(s.maxPremium !== undefined ? { maxPremium: formatBudget(s.maxPremium) } : {}),
|
|
246
|
+
...(s.optionStrategy ? { optionStrategy: s.optionStrategy } : {}),
|
|
247
|
+
...(s.costBasis !== undefined ? { costBasis: formatBudget(s.costBasis) } : {}),
|
|
248
|
+
...(s.shareQuantity !== undefined ? { shareQuantity: `${s.shareQuantity} shares` } : {}),
|
|
249
|
+
...(s.catalystSymbols?.length ? { catalystSymbols: s.catalystSymbols.join(", ") } : {}),
|
|
219
250
|
},
|
|
220
251
|
sources as Record<string, SlotSource | undefined>,
|
|
221
252
|
workflowConstraints,
|
|
222
253
|
);
|
|
223
254
|
|
|
255
|
+
const coveredCallContext = [
|
|
256
|
+
s.optionStrategy ? `\n- Option strategy: ${s.optionStrategy}${tag(sources.optionStrategy)}` : "",
|
|
257
|
+
s.costBasis !== undefined ? `\n- Cost basis: ${formatBudget(s.costBasis)} (Position cost basis: ${formatBudget(s.costBasis)})${tag(sources.costBasis)}` : "",
|
|
258
|
+
s.shareQuantity !== undefined ? `\n- Share quantity: ${s.shareQuantity} shares${tag(sources.shareQuantity)}` : "",
|
|
259
|
+
s.catalystSymbols?.length ? `\n- Catalyst/context tickers: ${s.catalystSymbols.join(", ")}${tag(sources.catalystSymbols)}` : "",
|
|
260
|
+
].join("");
|
|
261
|
+
|
|
262
|
+
const isProtectivePutContext = s.optionStrategy === "protective_put";
|
|
263
|
+
const isCoveredCallContext = !isProtectivePutContext && (
|
|
264
|
+
s.optionStrategy === "covered_call" ||
|
|
265
|
+
s.costBasis !== undefined ||
|
|
266
|
+
(s.catalystSymbols?.length ?? 0) > 0
|
|
267
|
+
);
|
|
268
|
+
const coveredCallInstructions = isCoveredCallContext
|
|
269
|
+
? `
|
|
270
|
+
Covered-call sale guidance:
|
|
271
|
+
- Treat this as selling covered calls against an existing ${s.symbol} share position, not buying calls.
|
|
272
|
+
- Treat ${s.symbol} as the option-chain underlying.
|
|
273
|
+
- Because the user phrased ${s.symbol} as an existing holding, briefly state that you are treating ${s.symbol} as the held ticker. If they meant memory exposure or a different ticker, tell them to clarify and do not silently switch to another underlying.
|
|
274
|
+
- Do not substitute catalyst/context tickers as the option-chain underlying.
|
|
275
|
+
- Use catalyst/context tickers only to frame event risk, sympathy moves, and whether a nearer expiration is appropriate.
|
|
276
|
+
- Rank by premium collected, strike above cost basis, assignment risk, event risk, and live liquidity.
|
|
277
|
+
- Do not describe max loss as the option premium paid. Covered-call sale risks are assignment/capped upside, share-price downside in the owned stock, IV/event risk, and poor exit liquidity.
|
|
278
|
+
- If the option-chain tool reports closed_market_or_stale_quotes, do not treat zero bid/ask as confirmed live illiquidity; say the chain was checked outside regular options trading and recheck after regular options trading opens.
|
|
279
|
+
- If retrieved contracts have zero bid/ask, zero open interest, or otherwise unusable live quotes, the final answer MUST still include "Best action:" and "Conditional candidate:".
|
|
280
|
+
- In that fallback, "Best action:" should be no trade unless the user's broker shows a real bid, and "Conditional candidate:" should be a strike above cost basis labeled conditional on live bid/ask.
|
|
281
|
+
`
|
|
282
|
+
: "";
|
|
283
|
+
const protectivePutInstructions = isProtectivePutContext
|
|
284
|
+
? `
|
|
285
|
+
Protective-put hedge guidance:
|
|
286
|
+
- Treat this as buying puts to hedge an existing long ${s.symbol} share position, not buying calls.
|
|
287
|
+
- Treat ${s.symbol} as the option-chain underlying.
|
|
288
|
+
- Rank put contracts by protection per dollar of premium: expiration fit, hedge floor, moneyness, liquidity, and premium as a percent of the stock position.
|
|
289
|
+
- For "doesn't cost too much" or similar cost-sensitive language, prefer liquid puts modestly below the current stock price before far-OTM lottery hedges; explain the tradeoff between cheaper premium and weaker protection.
|
|
290
|
+
- If share quantity is provided, use 1 put contract per 100 shares when discussing coverage and contract count.
|
|
291
|
+
- If share quantity is provided, state total premium for the required number of contracts in the ranked table or top-pick explanation.
|
|
292
|
+
- Include the hedge floor: approximate protected stock value at strike, net of premium where possible.
|
|
293
|
+
- Mention lower-cost alternatives such as a collar or put spread when outright put premium is high.
|
|
294
|
+
- Long protective puts have premium/decay risk and exercise/exit choices; do not frame assignment risk like a short option sale.
|
|
295
|
+
`
|
|
296
|
+
: "";
|
|
297
|
+
const topPickExplanation = isCoveredCallContext
|
|
298
|
+
? `Explain why the top pick is ranked #1. For covered calls with a cost basis, include the effective assignment sale price (strike + premium collected) and compare it with the ${s.costBasis !== undefined ? formatBudget(s.costBasis) : "user's"} cost basis.`
|
|
299
|
+
: "Explain why the top pick is ranked #1.";
|
|
300
|
+
|
|
224
301
|
return `Current date: ${dateStr}
|
|
225
302
|
Do NOT invent or assume a different current date.${expirationSection}
|
|
226
303
|
|
|
@@ -229,51 +306,128 @@ Screen and rank options contracts for ${s.symbol}:
|
|
|
229
306
|
- DTE target: ${s.dteTarget}${tag(sources.dteTarget)}
|
|
230
307
|
- Objective: ${s.objective}${tag(sources.objective)}
|
|
231
308
|
- Moneyness: ${s.moneynessPreference}${tag(sources.moneynessPreference)}
|
|
232
|
-
- Liquidity: ${s.liquidityMinimum}${tag(sources.liquidityMinimum)}${s.budget ? `\n- Budget: ${formatBudget(s.budget)}` : ""}${s.maxPremium ? `\n- Max premium: ${formatBudget(s.maxPremium)}` : ""}
|
|
309
|
+
- Liquidity: ${s.liquidityMinimum}${tag(sources.liquidityMinimum)}${coveredCallContext}${s.budget ? `\n- Budget: ${formatBudget(s.budget)}` : ""}${s.maxPremium ? `\n- Max premium: ${formatBudget(s.maxPremium)}` : ""}
|
|
233
310
|
|
|
234
311
|
Steps:
|
|
235
312
|
1. Use get_stock_quote for ${s.symbol} to get current price and recent movement.
|
|
236
313
|
2. Use get_option_chain for ${s.symbol} to get the full chain with Greeks. If you filter by contract type, pass \`type: "call"\` or \`type: "put"\` in lowercase.
|
|
237
|
-
3. Filter contracts matching: ${s.direction === "bullish" ? "calls" : "puts"}, DTE near ${s.dteTarget}, ${s.moneynessPreference} strikes.
|
|
238
|
-
4. Rank by ${s.objective}: balance premium cost, delta exposure, and probability of profit.
|
|
314
|
+
3. Filter contracts matching: ${s.direction === "bullish" && !isProtectivePutContext ? "calls" : "puts"}, DTE near ${s.dteTarget}, ${s.moneynessPreference} strikes.
|
|
315
|
+
4. ${isProtectivePutContext ? "Rank by hedge quality: protection per dollar of premium, expiration fit, moneyness, liquidity, and hedge floor." : `Rank by ${s.objective}: balance premium cost, delta exposure, and probability of profit.`}${s.maxPremium !== undefined ? ` Do not rank contracts above the user's max premium of ${formatBudget(s.maxPremium)} unless no contracts under that cap are liquid; if so, say the cap could not be met.` : ""}
|
|
239
316
|
5. Filter for ${s.liquidityMinimum}: high open interest and tight bid-ask spread.
|
|
317
|
+
${s.optionStrategy === "covered_call" ? `6. Covered call framing: treat option premium as premium received, not paid. Use the user's cost basis when provided, and include return-if-assigned and assignment/downside risk instead of long-call max-loss framing.
|
|
318
|
+
` : ""}${isCoveredCallContext && s.costBasis !== undefined ? `Cost-basis math: if assigned, share gain/loss is strike minus ${formatBudget(s.costBasis)} before premium. Total return if assigned is (strike - cost basis + premium received) / cost basis.
|
|
319
|
+
` : ""}
|
|
240
320
|
${longDatedInstructions}
|
|
321
|
+
${coveredCallInstructions}
|
|
322
|
+
${protectivePutInstructions}
|
|
241
323
|
${rankingConstraints}
|
|
242
324
|
${disclosureBlock}
|
|
243
325
|
|
|
244
326
|
Response format:
|
|
245
327
|
- Start with the assumptions block above exactly as written. Do not relabel source attribution anywhere else in your response.
|
|
246
|
-
-
|
|
247
|
-
-
|
|
248
|
-
-
|
|
328
|
+
- ${isCoveredCallContext ? `Start with an Interpretation line: "Interpretation: Treating ${s.symbol} as the held ticker because you phrased it as an existing position. If you meant ${s.symbol} as memory exposure or another ticker, clarify before trading."` : isProtectivePutContext ? `Start with an Interpretation line: "Interpretation: Treating this as buying protective puts on an existing long ${s.symbol} share position."` : "State the interpretation only if the user's requested underlying is ambiguous."}
|
|
329
|
+
- Present top 3-5 ranked contracts in a table: strike, expiry, premium, delta, gamma, theta, vega, rho, IV, OI, bid-ask spread${isProtectivePutContext ? ", hedge floor, premium % of position" : ""}.
|
|
330
|
+
- ${topPickExplanation}
|
|
331
|
+
- Verify bid/ask and open interest in the user's broker before trading, even when OC shows live values.
|
|
332
|
+
- Include ${isCoveredCallContext ? "covered-call sale risks (assignment/capped upside, share-price downside in the owned stock, IV/event risk, exit liquidity). Do not describe max loss as the option premium paid" : isProtectivePutContext ? "protective-put risks (premium decay/cost, imperfect hedge before the strike, liquidity, and opportunity cost). Do not discuss short-option assignment risk" : "risk caveats (max loss = premium, IV crush risk, time decay)"}.`;
|
|
249
333
|
}
|
|
250
334
|
|
|
251
335
|
export function buildCompareAssetsPrompt(resolution: SlotResolution<CompareAssetsSlots>): string {
|
|
252
336
|
const symbols = resolution.resolved.symbols;
|
|
253
337
|
const symbolList = symbols.join(", ");
|
|
338
|
+
const timeHorizon = resolution.resolved.timeHorizon;
|
|
339
|
+
const includeSentiment = resolution.resolved.metrics?.includes("sentiment") ?? false;
|
|
340
|
+
const isMacroHedge = resolution.resolved.metrics?.includes("macro_hedge") ?? false;
|
|
341
|
+
const isInterestRateSensitive = resolution.resolved.metrics?.includes("interest_rates") ?? false;
|
|
342
|
+
const isOverlapComparison = resolution.resolved.metrics?.includes("overlap") ?? false;
|
|
343
|
+
const sentimentStep = includeSentiment
|
|
344
|
+
? `\n6. Use get_sentiment_summary for each of: ${symbolList} to compare retail/news sentiment and note source availability.`
|
|
345
|
+
: "";
|
|
346
|
+
const interestRateStep = isInterestRateSensitive
|
|
347
|
+
? `\n${includeSentiment ? "7" : "6"}. Use get_economic_data for the current Fed funds backdrop. Treat this as historical/current context unless you also have explicit futures or forecast evidence.`
|
|
348
|
+
: "";
|
|
349
|
+
const sentimentMetric = includeSentiment ? ", sentiment score/summary" : "";
|
|
350
|
+
const interestRateGuidance = isInterestRateSensitive
|
|
351
|
+
? `
|
|
352
|
+
interest-rate comparison guidance:
|
|
353
|
+
- Separate the user's conditional premise from observed data: if the prompt says rates "start falling," state that the recommendation depends on why rates fall and whether market pricing confirms it.
|
|
354
|
+
- Give a compact scenario split: benign disinflation/soft landing, recession or earnings shock, and sticky inflation or renewed rate pressure. State which asset type should benefit in each case and why.
|
|
355
|
+
- Connect rates to asset mechanics: duration-like sensitivity of future earnings, cost of capital, earnings resilience, valuation multiples, and risk appetite.
|
|
356
|
+
- For ETF or fund comparisons, include concentration and sector-exposure risk when one asset is meaningfully narrower or more growth/technology-heavy than the other.
|
|
357
|
+
- If forward valuation, earnings estimates, or rate-futures evidence is unavailable, say that directly and avoid treating historical Fed funds data as a forecast.`
|
|
358
|
+
: "";
|
|
359
|
+
const overlapGuidance = isOverlapComparison
|
|
360
|
+
? `
|
|
361
|
+
ETF overlap guidance:
|
|
362
|
+
- Treat this as an ETF overlap and diversification question, not a generic return ranking.
|
|
363
|
+
- Lead with whether adding the second ETF creates a mega-cap technology tilt, growth-factor tilt, or real diversification.
|
|
364
|
+
- Explain that holdings overlap and sector concentration are not the same as correlation; correlation is supporting evidence, not the answer.
|
|
365
|
+
- Use provider top holdings and overlap weights when available. If provider coverage is partial or unavailable, say so directly and fall back to plain-language fund structure.
|
|
366
|
+
- Discuss top holdings, shared mega-cap names, sector concentration, and whether the position is a deliberate tilt or accidental duplication.
|
|
367
|
+
- avoid treating price, RSI, or generic risk metrics as the main answer.`
|
|
368
|
+
: "";
|
|
369
|
+
const macroHedgeSteps = isMacroHedge
|
|
370
|
+
? `
|
|
371
|
+
macro hedge decision guidance:
|
|
372
|
+
- Treat this as a hedge-role comparison, not a generic "which asset has better recent technicals" ranking.
|
|
373
|
+
- Prioritize what each asset hedges: inflation, falling real yields, USD weakness, geopolitical/systemic shocks, liquidity stress, and risk-asset drawdowns.
|
|
374
|
+
- Compare volatility, drawdown, and correlation regime stability. If a metric is unavailable for one asset, explain how that limits confidence instead of awarding the other asset by default.
|
|
375
|
+
- Use current macro evidence where available: real yields or Fed-rate direction, inflation trend, USD/liquidity backdrop, and risk-on/risk-off conditions.
|
|
376
|
+
- Include a compact scenario map for stagflation, rising real yields, liquidity crunch/risk-off, USD debasement, and geopolitical shock. State which asset is likely the better hedge in each scenario and why.
|
|
377
|
+
- End with conditional guidance: prefer the steadier hedge for capital preservation; prefer the higher-volatility asset only for debasement/asymmetric-upside exposure.`
|
|
378
|
+
: "";
|
|
379
|
+
const tableInstruction = isMacroHedge
|
|
380
|
+
? "- Present a comparison table with hedge-relevant columns: hedge role, macro drivers, volatility/drawdown evidence, correlation regime, liquidity/risk-on sensitivity, current data, and missing evidence."
|
|
381
|
+
: isOverlapComparison
|
|
382
|
+
? "- Present an ETF overlap table with columns: fund role, shared top holdings/overlap weight from provider when available, sector concentration, what exposure is duplicated, what exposure is new, and diversification implication."
|
|
383
|
+
: `- Present a comparison table with key metrics: price, P/E, revenue growth, profit margin, RSI, Sharpe, max drawdown${sentimentMetric}.
|
|
384
|
+
- Highlight which asset is stronger on each metric.`;
|
|
385
|
+
const technicalRiskSteps = isOverlapComparison
|
|
386
|
+
? `3. Use analyze_holdings_overlap with symbols [${symbolList}] to fetch provider top holdings and compute pairwise overlap by weight.
|
|
387
|
+
4. Use analyze_correlation across [${symbolList}] only as supporting diversification evidence; do not substitute correlation for holdings overlap.
|
|
388
|
+
5. Skip momentum/risk tool calls unless the user asks about timing or trade setup; the core question is top holdings and sector overlap.`
|
|
389
|
+
: `3. Use get_technical_indicators for each to compare momentum and trend.
|
|
390
|
+
4. Use analyze_risk for each to compare risk metrics.
|
|
391
|
+
5. Use analyze_correlation across [${symbolList}] to check diversification.`;
|
|
392
|
+
const horizonLine = timeHorizon ? `\nTime horizon: ${timeHorizon}` : "";
|
|
393
|
+
const horizonSteps = timeHorizon
|
|
394
|
+
? `
|
|
395
|
+
6. Adapt the comparison to the ${timeHorizon} horizon: prioritize near-term catalysts, earnings/guidance, estimate revisions, sentiment, and forward-looking valuation evidence over long-term historical averages.
|
|
396
|
+
7. Use historical risk and technical metrics as context, but explain what they do and do not imply over ${timeHorizon}.`
|
|
397
|
+
: "";
|
|
398
|
+
const horizonResponse = timeHorizon
|
|
399
|
+
? `
|
|
400
|
+
- Start the verdict by directly answering whether the assets should compare for a ${timeHorizon} horizon and why.
|
|
401
|
+
- Prioritize the evidence that matters most over ${timeHorizon}: near-term catalysts, earnings/guidance, forward-looking valuation/estimates, sentiment, macro sensitivity, and company-specific risks.
|
|
402
|
+
- Call out evidence that is missing or unavailable, especially forward-looking estimates or company-specific catalysts.`
|
|
403
|
+
: "";
|
|
254
404
|
|
|
255
405
|
const disclosureBlock = buildDisclosureBlock(
|
|
256
|
-
{
|
|
406
|
+
{
|
|
407
|
+
symbols: symbolList,
|
|
408
|
+
...(timeHorizon ? { timeHorizon } : {}),
|
|
409
|
+
...(resolution.resolved.metrics ? { metrics: resolution.resolved.metrics.join(", ") } : {}),
|
|
410
|
+
},
|
|
257
411
|
resolution.sources as Record<string, SlotSource | undefined>,
|
|
258
412
|
);
|
|
259
413
|
|
|
260
414
|
return `Current date: ${todayStr()}
|
|
261
415
|
|
|
262
|
-
Compare these assets side by side: ${symbolList}
|
|
416
|
+
Compare these assets side by side: ${symbolList}${horizonLine}
|
|
263
417
|
|
|
264
418
|
Steps:
|
|
265
419
|
1. Use get_stock_quote for each of: ${symbolList}.
|
|
266
420
|
2. Use compare_companies with symbols [${symbols.map((s) => `"${s}"`).join(", ")}] for peer metrics. If some fundamentals are unavailable, continue the comparison with the available symbols and mark missing metrics as unavailable.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
421
|
+
${technicalRiskSteps}${sentimentStep}${interestRateStep}${horizonSteps}
|
|
422
|
+
${macroHedgeSteps}
|
|
423
|
+
${interestRateGuidance}
|
|
424
|
+
${overlapGuidance}
|
|
270
425
|
|
|
271
426
|
${disclosureBlock}
|
|
272
427
|
|
|
273
428
|
Response format:
|
|
274
429
|
- Start with the assumptions block above exactly as written. Do not relabel source attribution anywhere else in your response.
|
|
275
|
-
|
|
276
|
-
- Highlight which asset is stronger on each metric.
|
|
430
|
+
${tableInstruction}
|
|
277
431
|
- Provide a summary verdict: which is most attractive and why.
|
|
278
|
-
- Note any caveats (different sectors, market cap disparity, unavailable fundamentals, etc.)
|
|
432
|
+
- Note any caveats (different sectors, concentration, market cap disparity, unavailable fundamentals, unavailable forward-looking estimates, etc.).${horizonResponse}`;
|
|
279
433
|
}
|
|
@@ -27,6 +27,23 @@ function buildUrl(fn: string, params: Record<string, string>, apiKey: string): s
|
|
|
27
27
|
return `${BASE_URL}?${qs}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function throwIfApiMessage(data: unknown): void {
|
|
31
|
+
if (!data || typeof data !== "object") return;
|
|
32
|
+
|
|
33
|
+
const payload = data as Record<string, unknown>;
|
|
34
|
+
const message = payload.Note ?? payload.Information ?? payload["Error Message"];
|
|
35
|
+
if (typeof message !== "string" || message.length === 0) return;
|
|
36
|
+
|
|
37
|
+
const normalized = message.toLowerCase();
|
|
38
|
+
if (normalized.includes("api call frequency") || normalized.includes("rate limit")) {
|
|
39
|
+
throw new Error(`Alpha Vantage rate limited: ${message}`);
|
|
40
|
+
}
|
|
41
|
+
if (normalized.includes("invalid api")) {
|
|
42
|
+
throw new ProviderCredentialError("alpha_vantage", "stale");
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Alpha Vantage error: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
export async function getOverview(
|
|
31
48
|
symbol: string,
|
|
32
49
|
apiKey: string,
|
|
@@ -44,6 +61,7 @@ export async function getOverview(
|
|
|
44
61
|
|
|
45
62
|
const url = buildUrl("OVERVIEW", { symbol }, apiKey);
|
|
46
63
|
const data = await httpGet<Record<string, string>>(url);
|
|
64
|
+
throwIfApiMessage(data);
|
|
47
65
|
|
|
48
66
|
if (!data.Symbol) {
|
|
49
67
|
cache.set(missingCacheKey, "missing", MISSING_OVERVIEW_TTL);
|
|
@@ -93,6 +111,7 @@ export async function getEarnings(
|
|
|
93
111
|
|
|
94
112
|
const url = buildUrl("EARNINGS", { symbol }, apiKey);
|
|
95
113
|
const data = await httpGet<{ quarterlyEarnings: any[] }>(url);
|
|
114
|
+
throwIfApiMessage(data);
|
|
96
115
|
|
|
97
116
|
const quarterly = (data.quarterlyEarnings ?? []).slice(0, 8).map((e: any) => ({
|
|
98
117
|
date: e.fiscalDateEnding,
|
|
@@ -178,7 +197,9 @@ export async function getFinancials(
|
|
|
178
197
|
async function fetchStatement<T>(fn: string, symbol: string, apiKey: string): Promise<T> {
|
|
179
198
|
await rateLimiter.acquire("alphavantage");
|
|
180
199
|
const url = buildUrl(fn, { symbol }, apiKey);
|
|
181
|
-
|
|
200
|
+
const data = await httpGet<T>(url);
|
|
201
|
+
throwIfApiMessage(data);
|
|
202
|
+
return data;
|
|
182
203
|
}
|
|
183
204
|
|
|
184
205
|
export async function getGlobalQuote(
|
|
@@ -194,6 +215,7 @@ export async function getGlobalQuote(
|
|
|
194
215
|
|
|
195
216
|
const url = buildUrl("GLOBAL_QUOTE", { symbol }, apiKey);
|
|
196
217
|
const data = await httpGet<{ "Global Quote": Record<string, string> }>(url);
|
|
218
|
+
throwIfApiMessage(data);
|
|
197
219
|
const gq = data["Global Quote"];
|
|
198
220
|
|
|
199
221
|
if (!gq || !gq["05. price"]) {
|
|
@@ -245,6 +267,7 @@ export async function getDailyHistory(
|
|
|
245
267
|
const outputsize = daysNeeded > 100 ? "full" : "compact";
|
|
246
268
|
const url = buildUrl("TIME_SERIES_DAILY", { symbol, outputsize }, apiKey);
|
|
247
269
|
const data = await httpGet<{ "Time Series (Daily)": Record<string, Record<string, string>> }>(url);
|
|
270
|
+
throwIfApiMessage(data);
|
|
248
271
|
|
|
249
272
|
const timeSeries = data["Time Series (Daily)"];
|
|
250
273
|
if (!timeSeries) {
|
|
@@ -2,6 +2,8 @@ import { httpGet } from "../infra/http-client.js";
|
|
|
2
2
|
import { cache, TTL } from "../infra/cache.js";
|
|
3
3
|
|
|
4
4
|
const EFTS_BASE = "https://efts.sec.gov/LATEST/search-index";
|
|
5
|
+
const COMPANY_TICKERS_URL = "https://www.sec.gov/files/company_tickers.json";
|
|
6
|
+
const SUBMISSIONS_BASE = "https://data.sec.gov/submissions";
|
|
5
7
|
|
|
6
8
|
export interface SECFiling {
|
|
7
9
|
formType: string;
|
|
@@ -10,6 +12,14 @@ export interface SECFiling {
|
|
|
10
12
|
entityName: string;
|
|
11
13
|
accessionNumber: string;
|
|
12
14
|
url: string;
|
|
15
|
+
primaryDocumentUrl?: string;
|
|
16
|
+
items?: string[];
|
|
17
|
+
evidenceSnippets?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SearchFilingsOptions {
|
|
21
|
+
includeSnippets?: boolean;
|
|
22
|
+
snippetLimitPerFiling?: number;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
interface EFTSResponse {
|
|
@@ -23,20 +33,52 @@ interface EFTSResponse {
|
|
|
23
33
|
display_names: string[];
|
|
24
34
|
period_ending: string;
|
|
25
35
|
ciks: string[];
|
|
36
|
+
items?: string[];
|
|
26
37
|
};
|
|
27
38
|
}>;
|
|
28
39
|
};
|
|
29
40
|
}
|
|
30
41
|
|
|
42
|
+
interface CompanyTickerEntry {
|
|
43
|
+
cik_str: number;
|
|
44
|
+
ticker: string;
|
|
45
|
+
title: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SubmissionsResponse {
|
|
49
|
+
name: string;
|
|
50
|
+
tickers?: string[];
|
|
51
|
+
filings: {
|
|
52
|
+
recent: {
|
|
53
|
+
accessionNumber: string[];
|
|
54
|
+
filingDate: string[];
|
|
55
|
+
reportDate: string[];
|
|
56
|
+
form: string[];
|
|
57
|
+
primaryDocument: string[];
|
|
58
|
+
items?: string[];
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
31
63
|
export async function searchFilings(
|
|
32
64
|
ticker: string,
|
|
33
65
|
formTypes: string[] = ["10-K", "10-Q", "8-K"],
|
|
34
66
|
limit: number = 10,
|
|
67
|
+
options: SearchFilingsOptions = {},
|
|
35
68
|
): Promise<SECFiling[]> {
|
|
36
|
-
const cacheKey = `sec:${ticker}:${formTypes.join(",")}:${limit}`;
|
|
69
|
+
const cacheKey = `sec:${ticker}:${formTypes.join(",")}:${limit}:${options.includeSnippets ? "snippets" : "metadata"}`;
|
|
37
70
|
const cached = cache.get<SECFiling[]>(cacheKey);
|
|
38
71
|
if (cached) return cached;
|
|
39
72
|
|
|
73
|
+
if (options.includeSnippets) {
|
|
74
|
+
const submissions = await searchFilingsFromCompanySubmissions(ticker, formTypes, limit).catch(() => []);
|
|
75
|
+
if (submissions.length > 0) {
|
|
76
|
+
await enrichWithEvidenceSnippets(submissions, options.snippetLimitPerFiling ?? 3);
|
|
77
|
+
cache.set(cacheKey, submissions, TTL.FUNDAMENTALS);
|
|
78
|
+
return submissions;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
40
82
|
const params = new URLSearchParams({
|
|
41
83
|
q: ticker,
|
|
42
84
|
forms: formTypes.join(","),
|
|
@@ -60,12 +102,15 @@ export async function searchFilings(
|
|
|
60
102
|
const src = hit._source;
|
|
61
103
|
const accession = src.adsh;
|
|
62
104
|
if (!accession || seen.has(accession)) continue;
|
|
105
|
+
if (!matchesTicker(src.display_names, ticker)) continue;
|
|
63
106
|
seen.add(accession);
|
|
64
107
|
|
|
65
108
|
const cik = src.ciks?.[0] ?? "";
|
|
66
109
|
const displayName = src.display_names?.[0] ?? "";
|
|
67
110
|
// Extract entity name from display format: "APPLE INC (AAPL) (CIK 0000320193)"
|
|
68
111
|
const entityName = displayName.split("(")[0]?.trim() ?? displayName;
|
|
112
|
+
const archiveDir = buildEdgarArchiveDir(cik, accession);
|
|
113
|
+
const primaryDocumentUrl = buildPrimaryDocumentUrl(archiveDir, hit._id);
|
|
69
114
|
|
|
70
115
|
filings.push({
|
|
71
116
|
formType: src.form ?? "",
|
|
@@ -73,20 +118,102 @@ export async function searchFilings(
|
|
|
73
118
|
periodOfReport: src.period_ending ?? "",
|
|
74
119
|
entityName,
|
|
75
120
|
accessionNumber: accession,
|
|
76
|
-
url:
|
|
121
|
+
url: `${archiveDir}/${accession}-index.htm`,
|
|
122
|
+
primaryDocumentUrl,
|
|
123
|
+
items: src.items ?? [],
|
|
77
124
|
});
|
|
78
125
|
|
|
79
126
|
if (filings.length >= limit) break;
|
|
80
127
|
}
|
|
81
128
|
|
|
129
|
+
if (options.includeSnippets) {
|
|
130
|
+
await enrichWithEvidenceSnippets(filings, options.snippetLimitPerFiling ?? 3);
|
|
131
|
+
}
|
|
132
|
+
|
|
82
133
|
cache.set(cacheKey, filings, TTL.FUNDAMENTALS);
|
|
83
134
|
return filings;
|
|
84
135
|
}
|
|
85
136
|
|
|
86
|
-
function
|
|
137
|
+
async function searchFilingsFromCompanySubmissions(
|
|
138
|
+
ticker: string,
|
|
139
|
+
formTypes: string[],
|
|
140
|
+
limit: number,
|
|
141
|
+
): Promise<SECFiling[]> {
|
|
142
|
+
const company = await resolveCompanyTicker(ticker);
|
|
143
|
+
if (!company) return [];
|
|
144
|
+
|
|
145
|
+
const cik = String(company.cik_str).padStart(10, "0");
|
|
146
|
+
const data = await httpGet<SubmissionsResponse>(`${SUBMISSIONS_BASE}/CIK${cik}.json`, {
|
|
147
|
+
headers: { "User-Agent": "OpenCandle/1.0 (financial analysis agent)" },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const filings: SECFiling[] = [];
|
|
151
|
+
const recent = data.filings.recent;
|
|
152
|
+
for (let i = 0; i < recent.form.length && filings.length < limit; i++) {
|
|
153
|
+
const formType = recent.form[i] ?? "";
|
|
154
|
+
if (!formTypes.includes(formType)) continue;
|
|
155
|
+
const accession = recent.accessionNumber[i];
|
|
156
|
+
if (!accession) continue;
|
|
157
|
+
const archiveDir = buildEdgarArchiveDir(cik, accession);
|
|
158
|
+
const primaryDocument = recent.primaryDocument[i];
|
|
159
|
+
filings.push({
|
|
160
|
+
formType,
|
|
161
|
+
filedDate: recent.filingDate[i] ?? "",
|
|
162
|
+
periodOfReport: recent.reportDate[i] ?? "",
|
|
163
|
+
entityName: data.name || company.title,
|
|
164
|
+
accessionNumber: accession,
|
|
165
|
+
url: `${archiveDir}/${accession}-index.htm`,
|
|
166
|
+
primaryDocumentUrl: primaryDocument ? `${archiveDir}/${primaryDocument}` : undefined,
|
|
167
|
+
items: splitFilingItems(recent.items?.[i]),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return filings;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function resolveCompanyTicker(ticker: string): Promise<CompanyTickerEntry | undefined> {
|
|
174
|
+
const cacheKey = "sec:company-tickers";
|
|
175
|
+
let companies = cache.get<CompanyTickerEntry[]>(cacheKey);
|
|
176
|
+
if (!companies) {
|
|
177
|
+
const data = await httpGet<Record<string, CompanyTickerEntry>>(COMPANY_TICKERS_URL, {
|
|
178
|
+
headers: { "User-Agent": "OpenCandle/1.0 (financial analysis agent)" },
|
|
179
|
+
});
|
|
180
|
+
companies = Object.values(data);
|
|
181
|
+
cache.set(cacheKey, companies, TTL.FUNDAMENTALS);
|
|
182
|
+
}
|
|
183
|
+
const normalized = ticker.toUpperCase();
|
|
184
|
+
return companies.find((company) => company.ticker.toUpperCase() === normalized);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function splitFilingItems(raw: string | undefined): string[] {
|
|
188
|
+
return raw
|
|
189
|
+
? raw.split(",").map((item) => item.trim()).filter(Boolean)
|
|
190
|
+
: [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function matchesTicker(displayNames: string[] | undefined, ticker: string): boolean {
|
|
194
|
+
const normalized = ticker.toUpperCase();
|
|
195
|
+
return (displayNames ?? []).some((name) => {
|
|
196
|
+
const tickerGroups = name.match(/\(([^)]*)\)/g) ?? [];
|
|
197
|
+
return tickerGroups.some((group) =>
|
|
198
|
+
group
|
|
199
|
+
.slice(1, -1)
|
|
200
|
+
.split(",")
|
|
201
|
+
.map((part) => part.trim().toUpperCase())
|
|
202
|
+
.includes(normalized),
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildEdgarArchiveDir(cik: string, accession: string): string {
|
|
87
208
|
const cikNum = cik.replace(/^0+/, "");
|
|
88
209
|
const accessionNoDash = accession.replace(/-/g, "");
|
|
89
|
-
return `https://www.sec.gov/Archives/edgar/data/${cikNum}/${accessionNoDash}
|
|
210
|
+
return `https://www.sec.gov/Archives/edgar/data/${cikNum}/${accessionNoDash}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildPrimaryDocumentUrl(archiveDir: string, hitId: string): string | undefined {
|
|
214
|
+
const fileName = hitId.split(":")[1];
|
|
215
|
+
if (!fileName || !/\.(?:htm|html|txt)$/i.test(fileName)) return undefined;
|
|
216
|
+
return `${archiveDir}/${fileName}`;
|
|
90
217
|
}
|
|
91
218
|
|
|
92
219
|
function getDateYearsAgo(years: number): string {
|
|
@@ -94,3 +221,92 @@ function getDateYearsAgo(years: number): string {
|
|
|
94
221
|
d.setFullYear(d.getFullYear() - years);
|
|
95
222
|
return d.toISOString().split("T")[0];
|
|
96
223
|
}
|
|
224
|
+
|
|
225
|
+
async function enrichWithEvidenceSnippets(filings: SECFiling[], limitPerFiling: number): Promise<void> {
|
|
226
|
+
await Promise.all(
|
|
227
|
+
filings.map(async (filing) => {
|
|
228
|
+
if (!filing.primaryDocumentUrl) return;
|
|
229
|
+
try {
|
|
230
|
+
const raw = await fetchText(filing.primaryDocumentUrl);
|
|
231
|
+
filing.evidenceSnippets = extractEvidenceSnippets(raw, limitPerFiling);
|
|
232
|
+
} catch {
|
|
233
|
+
filing.evidenceSnippets = [];
|
|
234
|
+
}
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function fetchText(url: string): Promise<string> {
|
|
240
|
+
const response = await fetch(url, {
|
|
241
|
+
headers: { "User-Agent": "OpenCandle/1.0 (financial analysis agent)" },
|
|
242
|
+
});
|
|
243
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
244
|
+
return response.text();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const EVIDENCE_KEYWORDS = [
|
|
248
|
+
"risk factor",
|
|
249
|
+
"legal proceedings",
|
|
250
|
+
"litigation",
|
|
251
|
+
"regulatory",
|
|
252
|
+
"regulation",
|
|
253
|
+
"revenue",
|
|
254
|
+
"concentration",
|
|
255
|
+
"management's discussion",
|
|
256
|
+
"management discussion",
|
|
257
|
+
"liquidity",
|
|
258
|
+
"outlook",
|
|
259
|
+
"guidance",
|
|
260
|
+
"material weakness",
|
|
261
|
+
"going concern",
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
function extractEvidenceSnippets(raw: string, limit: number): string[] {
|
|
265
|
+
const text = stripFilingMarkup(raw);
|
|
266
|
+
const snippets: string[] = [];
|
|
267
|
+
const lower = text.toLowerCase();
|
|
268
|
+
|
|
269
|
+
for (const keyword of EVIDENCE_KEYWORDS) {
|
|
270
|
+
let searchFrom = 0;
|
|
271
|
+
while (searchFrom < lower.length) {
|
|
272
|
+
const index = lower.indexOf(keyword, searchFrom);
|
|
273
|
+
if (index === -1) break;
|
|
274
|
+
searchFrom = index + keyword.length;
|
|
275
|
+
|
|
276
|
+
const start = Math.max(0, index - 220);
|
|
277
|
+
const end = Math.min(text.length, index + keyword.length + 420);
|
|
278
|
+
const snippet = text.slice(start, end).replace(/\s+/g, " ").trim();
|
|
279
|
+
if (
|
|
280
|
+
snippet &&
|
|
281
|
+
!isLikelyTableOfContentsSnippet(snippet) &&
|
|
282
|
+
!snippets.some((existing) => existing.includes(snippet.slice(0, 80)))
|
|
283
|
+
) {
|
|
284
|
+
snippets.push(snippet);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (snippets.length >= limit) break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return snippets;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function stripFilingMarkup(raw: string): string {
|
|
295
|
+
return raw
|
|
296
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
|
297
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
|
|
298
|
+
.replace(/<[^>]+>/g, " ")
|
|
299
|
+
.replace(/ /gi, " ")
|
|
300
|
+
.replace(/&/gi, "&")
|
|
301
|
+
.replace(/ /g, " ")
|
|
302
|
+
.replace(/’/g, "'")
|
|
303
|
+
.replace(/“|”/g, "\"")
|
|
304
|
+
.replace(/\s+/g, " ")
|
|
305
|
+
.trim();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function isLikelyTableOfContentsSnippet(snippet: string): boolean {
|
|
309
|
+
const lower = snippet.toLowerCase();
|
|
310
|
+
const itemHeadingCount = lower.match(/\bitem\s+\d+[a-z]?\b/g)?.length ?? 0;
|
|
311
|
+
return lower.includes("table of contents") && itemHeadingCount >= 2;
|
|
312
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { search, searchNews, SafeSearchType, SearchTimeType } from "duck-duck-scrape";
|
|
2
|
-
import type { SearchResult
|
|
2
|
+
import type { SearchResult } from "duck-duck-scrape";
|
|
3
3
|
import type { NewsResult } from "duck-duck-scrape";
|
|
4
4
|
import { httpGet, HttpError } from "../infra/http-client.js";
|
|
5
5
|
import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
|