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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getFundHoldings } from "../../providers/yahoo-finance.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import type {
|
|
6
|
+
FundHolding,
|
|
7
|
+
FundHoldings,
|
|
8
|
+
FundHoldingsOverlap,
|
|
9
|
+
FundOverlapPair,
|
|
10
|
+
SharedFundHolding,
|
|
11
|
+
} from "../../types/portfolio.js";
|
|
12
|
+
|
|
13
|
+
const params = Type.Object({
|
|
14
|
+
symbols: Type.Array(Type.String(), {
|
|
15
|
+
description: "Array of 2+ ETF or fund ticker symbols to compare holdings overlap, e.g. ['VOO','QQQ']",
|
|
16
|
+
minItems: 2,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const holdingsOverlapTool: AgentTool<typeof params, FundHoldingsOverlap | null> = {
|
|
21
|
+
name: "analyze_holdings_overlap",
|
|
22
|
+
label: "ETF Holdings Overlap",
|
|
23
|
+
description:
|
|
24
|
+
"Fetch top fund/ETF holdings and compute pairwise overlap by weight. Useful for ETF diversification and hidden concentration checks.",
|
|
25
|
+
parameters: params,
|
|
26
|
+
async execute(_toolCallId, args) {
|
|
27
|
+
const symbols = [...new Set(args.symbols.map((symbol) => symbol.toUpperCase()))];
|
|
28
|
+
if (symbols.length < 2) {
|
|
29
|
+
throw new Error("Need at least 2 symbols for holdings overlap analysis.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const results = await Promise.all(symbols.map(async (symbol) => ({
|
|
33
|
+
symbol,
|
|
34
|
+
result: await wrapProvider("yahoo", () => getFundHoldings(symbol)),
|
|
35
|
+
})));
|
|
36
|
+
const unavailable = results.flatMap((entry) =>
|
|
37
|
+
entry.result.status === "unavailable"
|
|
38
|
+
? [{ symbol: entry.symbol, reason: entry.result.reason }]
|
|
39
|
+
: []
|
|
40
|
+
);
|
|
41
|
+
if (unavailable.length > 0) {
|
|
42
|
+
const missing = unavailable.map((entry) => `${entry.symbol}: ${entry.reason}`).join("; ");
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: `⚠ Holdings overlap unavailable for one or more funds (${missing}).` }],
|
|
45
|
+
details: null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const funds = results.flatMap((entry) => entry.result.status === "ok" ? [entry.result.data] : []);
|
|
50
|
+
const overlap = computeHoldingsOverlap(funds);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: formatOverlap(overlap) }],
|
|
53
|
+
details: overlap,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function computeHoldingsOverlap(funds: FundHoldings[]): FundHoldingsOverlap {
|
|
59
|
+
const pairs: FundOverlapPair[] = [];
|
|
60
|
+
for (let i = 0; i < funds.length; i += 1) {
|
|
61
|
+
for (let j = i + 1; j < funds.length; j += 1) {
|
|
62
|
+
pairs.push(computePairOverlap(funds[i], funds[j]));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { funds, pairs };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function computePairOverlap(a: FundHoldings, b: FundHoldings): FundOverlapPair {
|
|
69
|
+
const aBySymbol = new Map(a.holdings.map((holding) => [holding.symbol, holding]));
|
|
70
|
+
const sharedHoldings: SharedFundHolding[] = [];
|
|
71
|
+
for (const bHolding of b.holdings) {
|
|
72
|
+
const aHolding = aBySymbol.get(bHolding.symbol);
|
|
73
|
+
if (!aHolding) continue;
|
|
74
|
+
const overlapWeight = roundWeight(Math.min(aHolding.weight, bHolding.weight));
|
|
75
|
+
sharedHoldings.push({
|
|
76
|
+
symbol: bHolding.symbol,
|
|
77
|
+
name: commonHoldingName(aHolding, bHolding),
|
|
78
|
+
weights: {
|
|
79
|
+
[a.symbol]: aHolding.weight,
|
|
80
|
+
[b.symbol]: bHolding.weight,
|
|
81
|
+
},
|
|
82
|
+
overlapWeight,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
sharedHoldings.sort((left, right) => right.overlapWeight - left.overlapWeight);
|
|
86
|
+
return {
|
|
87
|
+
symbols: [a.symbol, b.symbol],
|
|
88
|
+
overlapWeight: roundWeight(sharedHoldings.reduce((sum, holding) => sum + holding.overlapWeight, 0)),
|
|
89
|
+
sharedHoldings,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatOverlap(overlap: FundHoldingsOverlap): string {
|
|
94
|
+
const lines: string[] = ["**ETF/Fund Holdings Overlap**", ""];
|
|
95
|
+
for (const pair of overlap.pairs) {
|
|
96
|
+
lines.push(`${pair.symbols.join("/")} holdings overlap: ${formatPercent(pair.overlapWeight)}`);
|
|
97
|
+
const topShared = pair.sharedHoldings.slice(0, 5);
|
|
98
|
+
if (topShared.length > 0) {
|
|
99
|
+
lines.push(`Top shared holdings: ${topShared.map((holding) =>
|
|
100
|
+
`${holding.symbol} (${formatPercent(holding.overlapWeight)} overlap)`
|
|
101
|
+
).join(", ")}`);
|
|
102
|
+
} else {
|
|
103
|
+
lines.push("No shared top holdings found in provider coverage.");
|
|
104
|
+
}
|
|
105
|
+
lines.push("");
|
|
106
|
+
}
|
|
107
|
+
lines.push("Provider note: overlap uses provider top-holdings coverage, not full portfolio look-through unless the provider returns all holdings.");
|
|
108
|
+
return lines.join("\n").trimEnd();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function commonHoldingName(a: FundHolding, b: FundHolding): string {
|
|
112
|
+
if (a.name && a.name !== a.symbol) return a.name;
|
|
113
|
+
if (b.name && b.name !== b.symbol) return b.name;
|
|
114
|
+
return a.symbol;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatPercent(value: number): string {
|
|
118
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function roundWeight(value: number): number {
|
|
122
|
+
return Math.round(value * 10_000) / 10_000;
|
|
123
|
+
}
|
|
@@ -184,7 +184,7 @@ export const predictionsTool: AgentTool<typeof params> = {
|
|
|
184
184
|
description:
|
|
185
185
|
"Track your analysis predictions and measure accuracy over time. Record: save a directional prediction with conviction. Check: evaluate all predictions against current prices, compute hit rate and conviction-weighted accuracy. Inspired by ATLAS's Darwinian scoring approach.",
|
|
186
186
|
parameters: params,
|
|
187
|
-
async execute(
|
|
187
|
+
async execute(_toolCallId, args) {
|
|
188
188
|
if (args.action === "record") {
|
|
189
189
|
if (!args.symbol || !args.direction || !args.conviction || !args.entry_price) {
|
|
190
190
|
throw new Error("symbol, direction, conviction, and entry_price are required for record action.");
|
|
@@ -17,7 +17,7 @@ export const riskAnalysisTool: AgentTool<typeof params, RiskMetrics> = {
|
|
|
17
17
|
description:
|
|
18
18
|
"Compute risk metrics for a stock: annualized return, volatility, Sharpe ratio, max drawdown, and Value at Risk (95%). All computed locally from historical data.",
|
|
19
19
|
parameters: params,
|
|
20
|
-
async execute(
|
|
20
|
+
async execute(_toolCallId, args) {
|
|
21
21
|
const symbol = args.symbol.toUpperCase();
|
|
22
22
|
const period = args.period ?? "1y";
|
|
23
23
|
const result = await wrapProvider("yahoo", () => getHistory(symbol, period, "1d"));
|
|
@@ -51,7 +51,7 @@ export const portfolioTrackerTool: AgentTool<typeof params, PortfolioSummary | n
|
|
|
51
51
|
description:
|
|
52
52
|
"Track your portfolio of stocks and crypto. Add/remove positions with cost basis, or view current holdings with live P&L. For stocks use standard tickers (AAPL, MSFT). For crypto use the -USD suffix (BTC-USD, ETH-USD, SOL-USD). Use search_ticker first if you're unsure of the exact ticker. Data persisted to ~/.opencandle/portfolio.json.",
|
|
53
53
|
parameters: params,
|
|
54
|
-
async execute(
|
|
54
|
+
async execute(_toolCallId, args) {
|
|
55
55
|
const positions = loadPortfolio();
|
|
56
56
|
|
|
57
57
|
if (args.action === "add") {
|
|
@@ -54,7 +54,7 @@ export const watchlistTool: AgentTool<typeof params> = {
|
|
|
54
54
|
description:
|
|
55
55
|
"Manage your watchlist of stocks and crypto. Add symbols with optional target and stop prices, remove symbols, or check current prices against your alert levels. Data persisted to ~/.opencandle/watchlist.json.",
|
|
56
56
|
parameters: params,
|
|
57
|
-
async execute(
|
|
57
|
+
async execute(_toolCallId, args) {
|
|
58
58
|
const items = loadWatchlist();
|
|
59
59
|
|
|
60
60
|
if (args.action === "add") {
|
|
@@ -118,17 +118,22 @@ export const watchlistTool: AgentTool<typeof params> = {
|
|
|
118
118
|
items.map(async (item) => {
|
|
119
119
|
const result = await wrapProvider("yahoo", () => getQuote(item.symbol));
|
|
120
120
|
if (result.status === "unavailable") {
|
|
121
|
-
return { ...item, currentPrice: 0, alerts: [`UNAVAILABLE: ${result.reason}`] };
|
|
121
|
+
return { ...item, currentPrice: 0, alerts: [`UNAVAILABLE: ${result.reason}`], statuses: [] };
|
|
122
122
|
}
|
|
123
123
|
const quote = result.data;
|
|
124
124
|
const alerts: string[] = [];
|
|
125
|
+
const statuses: string[] = [];
|
|
125
126
|
if (item.targetPrice && quote.price >= item.targetPrice) {
|
|
126
127
|
alerts.push(`TARGET HIT: $${quote.price.toFixed(2)} >= $${item.targetPrice}`);
|
|
128
|
+
} else if (item.targetPrice) {
|
|
129
|
+
statuses.push(`Target pending: $${quote.price.toFixed(2)} < $${item.targetPrice}`);
|
|
127
130
|
}
|
|
128
131
|
if (item.stopPrice && quote.price <= item.stopPrice) {
|
|
129
132
|
alerts.push(`STOP ALERT: $${quote.price.toFixed(2)} fell below $${item.stopPrice}`);
|
|
133
|
+
} else if (item.stopPrice) {
|
|
134
|
+
statuses.push(`Stop OK: $${quote.price.toFixed(2)} > $${item.stopPrice}`);
|
|
130
135
|
}
|
|
131
|
-
return { ...item, currentPrice: quote.price, alerts };
|
|
136
|
+
return { ...item, currentPrice: quote.price, alerts, statuses };
|
|
132
137
|
}),
|
|
133
138
|
);
|
|
134
139
|
|
|
@@ -140,9 +145,10 @@ export const watchlistTool: AgentTool<typeof params> = {
|
|
|
140
145
|
|
|
141
146
|
for (const c of checks) {
|
|
142
147
|
const alertStr = c.alerts.length > 0 ? ` ** ${c.alerts.join(" | ")} **` : "";
|
|
148
|
+
const statusStr = c.statuses.length > 0 ? ` | ${c.statuses.join(" | ")}` : "";
|
|
143
149
|
const targetStr = c.targetPrice ? ` | Target: $${c.targetPrice}` : "";
|
|
144
150
|
const stopStr = c.stopPrice ? ` | Stop: $${c.stopPrice}` : "";
|
|
145
|
-
lines.push(` ${c.symbol}: $${c.currentPrice.toFixed(2)}${targetStr}${stopStr}${alertStr}`);
|
|
151
|
+
lines.push(` ${c.symbol}: $${c.currentPrice.toFixed(2)}${targetStr}${stopStr}${statusStr}${alertStr}`);
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
return {
|
|
@@ -36,7 +36,7 @@ export const redditSentimentTool: AgentTool<typeof params, RedditSentimentResult
|
|
|
36
36
|
description:
|
|
37
37
|
"Analyze sentiment from financial Reddit communities. Supports single subreddit, multi-subreddit, and topic filtering. Returns scored posts with comment analysis and trend context.",
|
|
38
38
|
parameters: params,
|
|
39
|
-
async execute(
|
|
39
|
+
async execute(_toolCallId, args) {
|
|
40
40
|
const limit = Math.min(args.limit ?? 25, 100);
|
|
41
41
|
const config = getConfig();
|
|
42
42
|
|
|
@@ -4,6 +4,7 @@ import { getSubredditPosts, getPostComments } from "../../providers/reddit.js";
|
|
|
4
4
|
import { getTwitterSentiment } from "../../providers/twitter.js";
|
|
5
5
|
import { searchWeb } from "../../providers/web-search.js";
|
|
6
6
|
import { getCompanyNews, finnhubDateRange } from "../../providers/finnhub.js";
|
|
7
|
+
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
7
8
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
8
9
|
import { getConfig } from "../../config.js";
|
|
9
10
|
import { TwitterAdapter } from "../../sentiment/adapters/twitter.js";
|
|
@@ -28,14 +29,13 @@ export const sentimentSummaryTool: AgentTool<typeof params> = {
|
|
|
28
29
|
description:
|
|
29
30
|
"Cross-source sentiment summary combining Twitter, Reddit, and web/news. Returns per-source scores, aggregate sentiment, and divergence detection.",
|
|
30
31
|
parameters: params,
|
|
31
|
-
async execute(
|
|
32
|
+
async execute(_toolCallId, args) {
|
|
32
33
|
const hours = args.hours ?? 24;
|
|
33
34
|
const config = getConfig();
|
|
34
35
|
const warnings: string[] = [];
|
|
35
36
|
const allRecords: SentinelRecord[] = [];
|
|
36
37
|
|
|
37
38
|
const twitterAdapter = new TwitterAdapter();
|
|
38
|
-
const redditAdapter = new RedditAdapter();
|
|
39
39
|
const webAdapter = new WebAdapter();
|
|
40
40
|
const finnhubAdapter = new FinnhubAdapter();
|
|
41
41
|
|
|
@@ -171,6 +171,15 @@ export const sentimentSummaryTool: AgentTool<typeof params> = {
|
|
|
171
171
|
lines.push("");
|
|
172
172
|
lines.push(`**Aggregate:** ${aggregate >= 0 ? "+" : ""}${aggregate.toFixed(2)} (${sentimentLabel(aggregate)})`);
|
|
173
173
|
|
|
174
|
+
const priceContext = await buildPriceContext(candidateTickers[0], aggregate);
|
|
175
|
+
if (priceContext) {
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push(priceContext);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push("Source-coverage risk: sentiment can be noisy and missing sources can skew the signal; treat this as supporting evidence, not a standalone buy/sell input.");
|
|
182
|
+
|
|
174
183
|
// Divergence
|
|
175
184
|
if (result.divergence && result.divergence.detected) {
|
|
176
185
|
lines.push("");
|
|
@@ -197,6 +206,57 @@ export const sentimentSummaryTool: AgentTool<typeof params> = {
|
|
|
197
206
|
},
|
|
198
207
|
};
|
|
199
208
|
|
|
209
|
+
async function buildPriceContext(symbol: string | undefined, aggregateSentiment: number): Promise<string | null> {
|
|
210
|
+
if (!symbol) return null;
|
|
211
|
+
try {
|
|
212
|
+
const quote = await getQuote(symbol);
|
|
213
|
+
const sign = quote.changePercent >= 0 ? "+" : "";
|
|
214
|
+
const direction = quote.changePercent > 0 ? "positive" : quote.changePercent < 0 ? "negative" : "flat";
|
|
215
|
+
const sentimentDirection = aggregateSentiment > 0 ? "positive" : aggregateSentiment < 0 ? "negative" : "neutral";
|
|
216
|
+
const relationship = sentimentDirection === "neutral" || direction === "flat" || sentimentDirection === direction
|
|
217
|
+
? "roughly aligns with price action"
|
|
218
|
+
: "diverges from price action";
|
|
219
|
+
const freshnessNote = formatQuoteFreshnessNote(quote.timestamp);
|
|
220
|
+
return `Price context: ${quote.symbol}: $${quote.price.toFixed(2)} (${sign}${quote.changePercent.toFixed(2)}%).${freshnessNote} The ${sentimentDirection} sentiment signal ${relationship}.`;
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatQuoteFreshnessNote(timestamp: number | undefined): string {
|
|
227
|
+
if (!timestamp) return "";
|
|
228
|
+
const quoteDate = new Date(timestamp);
|
|
229
|
+
if (Number.isNaN(quoteDate.getTime())) return "";
|
|
230
|
+
|
|
231
|
+
const now = new Date();
|
|
232
|
+
const quoteDay = quoteDate.toLocaleDateString("en-US", { timeZone: "America/New_York" });
|
|
233
|
+
const currentDay = now.toLocaleDateString("en-US", { timeZone: "America/New_York" });
|
|
234
|
+
const quoteStamp = quoteDate.toLocaleString("en-US", {
|
|
235
|
+
dateStyle: "medium",
|
|
236
|
+
timeStyle: "short",
|
|
237
|
+
timeZone: "America/New_York",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const weekday = new Intl.DateTimeFormat("en-US", {
|
|
241
|
+
weekday: "long",
|
|
242
|
+
timeZone: "America/New_York",
|
|
243
|
+
}).format(now);
|
|
244
|
+
const isWeekend = weekday === "Saturday" || weekday === "Sunday";
|
|
245
|
+
|
|
246
|
+
if (quoteDay === currentDay) {
|
|
247
|
+
const marketClosedNote = isWeekend
|
|
248
|
+
? " U.S. markets are closed today, so treat this as delayed or last available price context, not active intraday trading."
|
|
249
|
+
: "";
|
|
250
|
+
return ` Quote timestamp: ${quoteStamp} ET.${marketClosedNote}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const marketClosedNote = isWeekend
|
|
254
|
+
? " U.S. markets are closed today, so treat this as last trading-session price action."
|
|
255
|
+
: "";
|
|
256
|
+
|
|
257
|
+
return ` Last available quote timestamp: ${quoteStamp} ET.${marketClosedNote}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
200
260
|
async function fetchRedditCrossSubreddit(
|
|
201
261
|
query: string,
|
|
202
262
|
subreddits: string[],
|
|
@@ -24,7 +24,7 @@ export const twitterSentimentTool: AgentTool<typeof params, TwitterSentimentResu
|
|
|
24
24
|
description:
|
|
25
25
|
"Fetch recent tweets for a stock ticker or search query and compute engagement-weighted sentiment. Returns tweet data, sentiment score, and co-mentioned tickers. Requires a Twitter session via trigger_twitter_login.",
|
|
26
26
|
parameters: params,
|
|
27
|
-
async execute(
|
|
27
|
+
async execute(_toolCallId, args) {
|
|
28
28
|
const limit = Math.min(args.limit ?? 50, 200);
|
|
29
29
|
const hours = args.hours ?? 24;
|
|
30
30
|
|
|
@@ -79,6 +79,33 @@ function buildSoftDegradedPrefix(data: WebSearchEnvelope): string {
|
|
|
79
79
|
return tags.length === 0 ? "" : `${tags.join("\n")}\n\n`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function buildOfficialSourceGapPrefix(query: string, data: WebSearchEnvelope): string {
|
|
83
|
+
if (!hasOfficialFedSourceGap(query, data)) return "";
|
|
84
|
+
|
|
85
|
+
return [
|
|
86
|
+
"[OPENCANDLE_SOURCE_GAP source=fed_official evidence=missing remediation=\"verify against federalreserve.gov/FOMC before stating Fed announcements\"]",
|
|
87
|
+
"Hard source gap: no official Fed/FOMC source was returned. Do not present meeting announcements, votes, quotes, appointments, leadership changes, or named policy rationales as verified; treat results as market commentary only.",
|
|
88
|
+
"",
|
|
89
|
+
].join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasOfficialFedSourceGap(query: string, data: WebSearchEnvelope): boolean {
|
|
93
|
+
return isFedAnnouncementQuery(query) &&
|
|
94
|
+
!data.results.some((result) => isOfficialFedSource(result.source) || isOfficialFedSource(result.url));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isFedAnnouncementQuery(query: string): boolean {
|
|
98
|
+
const lower = query.toLowerCase();
|
|
99
|
+
const mentionsFed = /\b(?:fed|fomc|federal reserve)\b/.test(lower);
|
|
100
|
+
const asksOfficialFact = /\b(?:announcement|meeting|minutes|statement|decision|vote|chair|governor|appointment|leadership)\b/.test(lower);
|
|
101
|
+
return mentionsFed && asksOfficialFact;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isOfficialFedSource(value: string): boolean {
|
|
105
|
+
const lower = value.toLowerCase();
|
|
106
|
+
return lower.includes("federalreserve.gov") || lower.includes("fomc.gov");
|
|
107
|
+
}
|
|
108
|
+
|
|
82
109
|
export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
|
|
83
110
|
name: "search_web",
|
|
84
111
|
label: "Web Search",
|
|
@@ -87,7 +114,7 @@ export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
|
|
|
87
114
|
"NOT for real-time prices, historical data, fundamentals, macro data, SEC filings, or social sentiment — those have dedicated tools.",
|
|
88
115
|
parameters: params,
|
|
89
116
|
|
|
90
|
-
async execute(
|
|
117
|
+
async execute(_toolCallId, args) {
|
|
91
118
|
const query = args.query?.trim();
|
|
92
119
|
if (!query) {
|
|
93
120
|
return {
|
|
@@ -114,11 +141,12 @@ export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
|
|
|
114
141
|
|
|
115
142
|
if (data.resultCount === 0) {
|
|
116
143
|
const zeroPrefix = buildSoftDegradedPrefix(data);
|
|
144
|
+
const sourceGapPrefix = buildOfficialSourceGapPrefix(query, data);
|
|
117
145
|
return {
|
|
118
146
|
content: [
|
|
119
147
|
{
|
|
120
148
|
type: "text",
|
|
121
|
-
text: `${zeroPrefix}No results found for "${query}" (${category}, past ${freshness}).`,
|
|
149
|
+
text: `${zeroPrefix}${sourceGapPrefix}No results found for "${query}" (${category}, past ${freshness}).`,
|
|
122
150
|
},
|
|
123
151
|
],
|
|
124
152
|
details: data,
|
|
@@ -130,6 +158,8 @@ export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
|
|
|
130
158
|
: "";
|
|
131
159
|
|
|
132
160
|
const softDegradedPrefix = buildSoftDegradedPrefix(data);
|
|
161
|
+
const sourceGapPrefix = buildOfficialSourceGapPrefix(query, data);
|
|
162
|
+
const shouldOmitResults = hasOfficialFedSourceGap(query, data);
|
|
133
163
|
|
|
134
164
|
const header = `**Web Search** — ${data.resultCount} results for "${query}" (${category}, past ${freshness}, via ${data.provider})`;
|
|
135
165
|
const items = data.results.map((r) => {
|
|
@@ -139,8 +169,11 @@ export const webSearchTool: AgentTool<typeof params, WebSearchEnvelope> = {
|
|
|
139
169
|
const pub = r.published ? `Published: ${r.published}` : "Published: unknown";
|
|
140
170
|
return `• [${title}](${url}) — ${r.source}\n ${snippet}\n ${pub}`;
|
|
141
171
|
});
|
|
172
|
+
const body = shouldOmitResults
|
|
173
|
+
? "Non-official results were omitted from assistant-visible evidence for this Fed/FOMC announcement query. Verify against an official Federal Reserve or FOMC source before naming announcements or personnel changes."
|
|
174
|
+
: items.join("\n\n");
|
|
142
175
|
|
|
143
|
-
const text = `${softDegradedPrefix}${stalePrefix}${header}\n\n${
|
|
176
|
+
const text = `${softDegradedPrefix}${sourceGapPrefix}${stalePrefix}${header}\n\n${body}`;
|
|
144
177
|
|
|
145
178
|
return {
|
|
146
179
|
content: [{ type: "text", text }],
|
|
@@ -22,7 +22,7 @@ export const webSentimentTool: AgentTool<typeof params> = {
|
|
|
22
22
|
description:
|
|
23
23
|
"Analyze sentiment from web and news search results for a ticker or topic. Returns scored results with aggregate sentiment.",
|
|
24
24
|
parameters: params,
|
|
25
|
-
async execute(
|
|
25
|
+
async execute(_toolCallId, args) {
|
|
26
26
|
const freshness = args.freshness ?? "day";
|
|
27
27
|
const limit = Math.min(args.limit ?? 10, 20);
|
|
28
28
|
|
|
@@ -5,7 +5,7 @@ import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
|
5
5
|
import { computeSMA, computeRSI } from "./indicators.js";
|
|
6
6
|
import type { OHLCV } from "../../types/market.js";
|
|
7
7
|
|
|
8
|
-
export type Strategy = "sma_crossover" | "rsi_mean_reversion";
|
|
8
|
+
export type Strategy = "sma_crossover" | "sma_50_200_crossover" | "rsi_mean_reversion";
|
|
9
9
|
|
|
10
10
|
export interface BacktestResult {
|
|
11
11
|
strategy: string;
|
|
@@ -20,28 +20,32 @@ export interface BacktestResult {
|
|
|
20
20
|
|
|
21
21
|
export function runBacktest(bars: OHLCV[], strategy: Strategy): BacktestResult {
|
|
22
22
|
const closes = bars.map((b) => b.close);
|
|
23
|
-
const buyAndHoldReturn = closes.length > 1
|
|
24
|
-
? (closes[closes.length - 1] - closes[0]) / closes[0]
|
|
25
|
-
: 0;
|
|
26
23
|
|
|
27
24
|
if (strategy === "sma_crossover") {
|
|
28
|
-
return backtestSMACrossover(bars, closes);
|
|
25
|
+
return backtestSMACrossover(bars, closes, 20, 50, strategy);
|
|
26
|
+
}
|
|
27
|
+
if (strategy === "sma_50_200_crossover") {
|
|
28
|
+
return backtestSMACrossover(bars, closes, 50, 200, strategy);
|
|
29
29
|
}
|
|
30
30
|
return backtestRSIMeanReversion(bars, closes);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function backtestSMACrossover(
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
function backtestSMACrossover(
|
|
34
|
+
bars: OHLCV[],
|
|
35
|
+
closes: number[],
|
|
36
|
+
shortWindow: number,
|
|
37
|
+
longWindow: number,
|
|
38
|
+
strategyName: Strategy,
|
|
39
|
+
): BacktestResult {
|
|
40
|
+
const shortSma = computeSMA(closes, shortWindow);
|
|
41
|
+
const longSma = computeSMA(closes, longWindow);
|
|
36
42
|
|
|
37
|
-
if (
|
|
38
|
-
return emptyResult(
|
|
43
|
+
if (longSma.length === 0) {
|
|
44
|
+
return emptyResult(strategyName, closes);
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const offset20 = 19;
|
|
44
|
-
const offset50 = 49;
|
|
47
|
+
const shortOffset = shortWindow - 1;
|
|
48
|
+
const longOffset = longWindow - 1;
|
|
45
49
|
|
|
46
50
|
let position = false;
|
|
47
51
|
let entryPrice = 0;
|
|
@@ -50,19 +54,19 @@ function backtestSMACrossover(bars: OHLCV[], closes: number[]): BacktestResult {
|
|
|
50
54
|
let peak = 1.0;
|
|
51
55
|
let maxDd = 0;
|
|
52
56
|
|
|
53
|
-
for (let i = 0; i <
|
|
54
|
-
const barIdx = i +
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
57
|
+
for (let i = 0; i < longSma.length; i++) {
|
|
58
|
+
const barIdx = i + longOffset;
|
|
59
|
+
const shortSmaIdx = i + (longOffset - shortOffset);
|
|
60
|
+
const sShort = shortSma[shortSmaIdx];
|
|
61
|
+
const sLong = longSma[i];
|
|
58
62
|
const price = closes[barIdx];
|
|
59
63
|
|
|
60
|
-
if (!position &&
|
|
64
|
+
if (!position && sShort > sLong) {
|
|
61
65
|
// Buy signal
|
|
62
66
|
position = true;
|
|
63
67
|
entryPrice = price;
|
|
64
68
|
tradeLog.push({ type: "buy", date: bars[barIdx].date, price });
|
|
65
|
-
} else if (position &&
|
|
69
|
+
} else if (position && sShort < sLong) {
|
|
66
70
|
// Sell signal
|
|
67
71
|
const pnl = (price - entryPrice) / entryPrice;
|
|
68
72
|
equity *= 1 + pnl;
|
|
@@ -87,7 +91,7 @@ function backtestSMACrossover(bars: OHLCV[], closes: number[]): BacktestResult {
|
|
|
87
91
|
tradeLog.push({ type: "sell", date: bars[bars.length - 1].date, price: lastPrice, pnl });
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
return buildResult(
|
|
94
|
+
return buildResult(strategyName, equity - 1, closes, tradeLog, maxDd);
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
function backtestRSIMeanReversion(bars: OHLCV[], closes: number[]): BacktestResult {
|
|
@@ -187,8 +191,8 @@ function emptyResult(strategy: string, closes: number[]): BacktestResult {
|
|
|
187
191
|
const params = Type.Object({
|
|
188
192
|
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT, SPY)" }),
|
|
189
193
|
strategy: Type.Union(
|
|
190
|
-
[Type.Literal("sma_crossover"), Type.Literal("rsi_mean_reversion")],
|
|
191
|
-
{ description: "Strategy: sma_crossover (buy when SMA20 > SMA50, sell on reverse) or rsi_mean_reversion (buy when RSI < 30, sell when RSI > 70)" },
|
|
194
|
+
[Type.Literal("sma_crossover"), Type.Literal("sma_50_200_crossover"), Type.Literal("rsi_mean_reversion")],
|
|
195
|
+
{ description: "Strategy: sma_crossover (buy when SMA20 > SMA50, sell on reverse), sma_50_200_crossover (buy when SMA50 > SMA200, sell on reverse), or rsi_mean_reversion (buy when RSI < 30, sell when RSI > 70)" },
|
|
192
196
|
),
|
|
193
197
|
period: Type.Optional(
|
|
194
198
|
Type.String({ description: "Historical period to backtest: 1y, 2y, 5y. Default: 2y" }),
|
|
@@ -199,9 +203,9 @@ export const backtestTool: AgentTool<typeof params> = {
|
|
|
199
203
|
name: "backtest_strategy",
|
|
200
204
|
label: "Backtest Strategy",
|
|
201
205
|
description:
|
|
202
|
-
"Backtest a simple trading strategy against historical data. Supported strategies: SMA crossover (SMA20/SMA50) and RSI mean-reversion (buy <30, sell >70). Returns total return, win rate, max drawdown, and comparison to buy-and-hold.",
|
|
206
|
+
"Backtest a simple trading strategy against historical data. Supported strategies: SMA crossover (SMA20/SMA50), standard long-term SMA crossover (SMA50/SMA200), and RSI mean-reversion (buy <30, sell >70). Returns total return, win rate, max drawdown, and comparison to buy-and-hold.",
|
|
203
207
|
parameters: params,
|
|
204
|
-
async execute(
|
|
208
|
+
async execute(_toolCallId, args) {
|
|
205
209
|
const symbol = args.symbol.toUpperCase();
|
|
206
210
|
const period = args.period ?? "2y";
|
|
207
211
|
const historyResult = await wrapProvider("yahoo", () => getHistory(symbol, period, "1d"));
|
|
@@ -213,9 +217,10 @@ export const backtestTool: AgentTool<typeof params> = {
|
|
|
213
217
|
}
|
|
214
218
|
const bars = historyResult.data;
|
|
215
219
|
|
|
216
|
-
|
|
220
|
+
const minBars = requiredBarsForStrategy(args.strategy);
|
|
221
|
+
if (bars.length < minBars) {
|
|
217
222
|
return {
|
|
218
|
-
content: [{ type: "text", text: `Insufficient data for backtesting ${symbol} (need
|
|
223
|
+
content: [{ type: "text", text: `Insufficient data for backtesting ${symbol} (need ${minBars}+ days, got ${bars.length})` }],
|
|
219
224
|
details: null,
|
|
220
225
|
};
|
|
221
226
|
}
|
|
@@ -224,7 +229,7 @@ export const backtestTool: AgentTool<typeof params> = {
|
|
|
224
229
|
|
|
225
230
|
const outperformance = result.totalReturn - result.buyAndHoldReturn;
|
|
226
231
|
const lines = [
|
|
227
|
-
`**${symbol} Backtest: ${args.strategy}** (${bars[0].date} to ${bars[bars.length - 1].date}, ${bars.length} days)`,
|
|
232
|
+
`**${symbol} Backtest: ${strategyLabel(args.strategy)}** (${bars[0].date} to ${bars[bars.length - 1].date}, ${bars.length} days)`,
|
|
228
233
|
``,
|
|
229
234
|
`Strategy Return: ${(result.totalReturn * 100).toFixed(2)}%`,
|
|
230
235
|
`Buy & Hold Return: ${(result.buyAndHoldReturn * 100).toFixed(2)}%`,
|
|
@@ -244,3 +249,19 @@ export const backtestTool: AgentTool<typeof params> = {
|
|
|
244
249
|
};
|
|
245
250
|
},
|
|
246
251
|
};
|
|
252
|
+
|
|
253
|
+
function requiredBarsForStrategy(strategy: Strategy): number {
|
|
254
|
+
if (strategy === "sma_50_200_crossover") return 200;
|
|
255
|
+
return 60;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function strategyLabel(strategy: Strategy): string {
|
|
259
|
+
switch (strategy) {
|
|
260
|
+
case "sma_crossover":
|
|
261
|
+
return "SMA 20/50 Crossover";
|
|
262
|
+
case "sma_50_200_crossover":
|
|
263
|
+
return "SMA 50/200 Crossover";
|
|
264
|
+
case "rsi_mean_reversion":
|
|
265
|
+
return "RSI Mean Reversion";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -46,7 +46,7 @@ export const technicalIndicatorsTool: AgentTool<typeof params> = {
|
|
|
46
46
|
description:
|
|
47
47
|
"Compute technical indicators (SMA, EMA, RSI, MACD, Bollinger Bands) from historical price data. All computed locally — no API dependency.",
|
|
48
48
|
parameters: params,
|
|
49
|
-
async execute(
|
|
49
|
+
async execute(_toolCallId, args) {
|
|
50
50
|
const symbol = args.symbol.toUpperCase();
|
|
51
51
|
const range = args.range ?? "1y";
|
|
52
52
|
const result = await wrapProvider("yahoo", () => getHistory(symbol, range, "1d"));
|
|
@@ -68,8 +68,6 @@ export const technicalIndicatorsTool: AgentTool<typeof params> = {
|
|
|
68
68
|
|
|
69
69
|
const sma20 = computeSMA(closes, 20);
|
|
70
70
|
const sma50 = computeSMA(closes, 50);
|
|
71
|
-
const ema12 = computeEMA(closes, 12);
|
|
72
|
-
const ema26 = computeEMA(closes, 26);
|
|
73
71
|
const rsi = computeRSI(closes, 14);
|
|
74
72
|
const macd = computeMACD(closes);
|
|
75
73
|
const bb = computeBollingerBands(closes, 20, 2);
|
package/src/types/options.ts
CHANGED
|
@@ -21,6 +21,22 @@ export interface OptionContract {
|
|
|
21
21
|
greeks: Greeks;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export type OptionsMarketSession = "pre_market" | "regular" | "after_hours" | "closed";
|
|
25
|
+
|
|
26
|
+
export type OptionsBidAskState =
|
|
27
|
+
| "live_quotes"
|
|
28
|
+
| "closed_market_or_stale_quotes"
|
|
29
|
+
| "live_zero_bid_ask"
|
|
30
|
+
| "mixed_or_unknown";
|
|
31
|
+
|
|
32
|
+
export interface OptionsQuoteStatus {
|
|
33
|
+
marketSession: OptionsMarketSession;
|
|
34
|
+
bidAskState: OptionsBidAskState;
|
|
35
|
+
zeroBidAskContracts: number;
|
|
36
|
+
totalContracts: number;
|
|
37
|
+
warning?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
24
40
|
export interface OptionsChain {
|
|
25
41
|
symbol: string;
|
|
26
42
|
underlyingPrice: number;
|
|
@@ -31,5 +47,6 @@ export interface OptionsChain {
|
|
|
31
47
|
totalCallVolume: number;
|
|
32
48
|
totalPutVolume: number;
|
|
33
49
|
putCallRatio: number;
|
|
50
|
+
quoteStatus: OptionsQuoteStatus;
|
|
34
51
|
fetchedAt: string;
|
|
35
52
|
}
|
package/src/types/portfolio.ts
CHANGED
|
@@ -30,6 +30,38 @@ export interface RiskMetrics {
|
|
|
30
30
|
var95: number; // 95% Value at Risk (daily)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface FundHolding {
|
|
34
|
+
symbol: string;
|
|
35
|
+
name: string;
|
|
36
|
+
weight: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FundHoldings {
|
|
40
|
+
symbol: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
provider: string;
|
|
43
|
+
holdings: FundHolding[];
|
|
44
|
+
sectorWeights?: Record<string, number>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface SharedFundHolding {
|
|
48
|
+
symbol: string;
|
|
49
|
+
name: string;
|
|
50
|
+
weights: Record<string, number>;
|
|
51
|
+
overlapWeight: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface FundOverlapPair {
|
|
55
|
+
symbols: [string, string];
|
|
56
|
+
overlapWeight: number;
|
|
57
|
+
sharedHoldings: SharedFundHolding[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface FundHoldingsOverlap {
|
|
61
|
+
funds: FundHoldings[];
|
|
62
|
+
pairs: FundOverlapPair[];
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
export interface TechnicalIndicators {
|
|
34
66
|
symbol: string;
|
|
35
67
|
period: string;
|