opencandle 0.3.0 → 0.4.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/assets/logo.svg +187 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +38 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +10 -0
- package/dist/infra/browser.js +1 -0
- package/dist/infra/browser.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +1 -0
- package/dist/infra/native-dependencies.js +10 -0
- package/dist/infra/native-dependencies.js.map +1 -0
- package/dist/infra/node-version.d.ts +2 -0
- package/dist/infra/node-version.js +23 -0
- package/dist/infra/node-version.js.map +1 -0
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/sqlite.js +42 -4
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.d.ts +6 -0
- package/dist/memory/storage.js +3 -3
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.d.ts +8 -0
- package/dist/memory/tool-defaults.js +59 -0
- package/dist/memory/tool-defaults.js.map +1 -0
- package/dist/onboarding/connect.d.ts +13 -1
- package/dist/onboarding/connect.js +21 -10
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/prompt-user.d.ts +1 -1
- package/dist/onboarding/providers.d.ts +7 -0
- package/dist/onboarding/providers.js +6 -3
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +1 -1
- package/dist/pi/opencandle-extension.d.ts +7 -1
- package/dist/pi/opencandle-extension.js +186 -10
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-storage.d.ts +2 -0
- package/dist/pi/session-storage.js +5 -0
- package/dist/pi/session-storage.js.map +1 -0
- package/dist/pi/session.d.ts +4 -1
- package/dist/pi/session.js +25 -3
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.d.ts +1 -1
- package/dist/pi/setup.js +1 -1
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +2 -2
- package/dist/pi/tool-adapter.js +14 -1
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +22 -0
- package/dist/prompts/context-builder.js +45 -10
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/disclaimer.d.ts +6 -0
- package/dist/prompts/disclaimer.js +9 -0
- package/dist/prompts/disclaimer.js.map +1 -0
- package/dist/prompts/workflow-prompts.d.ts +8 -0
- package/dist/prompts/workflow-prompts.js +39 -5
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/yahoo-finance.js +70 -33
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/defaults.js +1 -1
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +3 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/router-llm-client.d.ts +11 -0
- package/dist/routing/router-llm-client.js +42 -0
- package/dist/routing/router-llm-client.js.map +1 -0
- package/dist/routing/router-prompt.d.ts +2 -0
- package/dist/routing/router-prompt.js +138 -0
- package/dist/routing/router-prompt.js.map +1 -0
- package/dist/routing/router-types.d.ts +62 -0
- package/dist/routing/router-types.js +2 -0
- package/dist/routing/router-types.js.map +1 -0
- package/dist/routing/router.d.ts +10 -0
- package/dist/routing/router.js +194 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/runtime/session-coordinator.d.ts +63 -3
- package/dist/runtime/session-coordinator.js +155 -4
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
- package/dist/runtime/tool-defaults-wrapper.js +25 -0
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
- package/dist/sentiment/store.js +5 -0
- package/dist/sentiment/store.js.map +1 -1
- package/dist/system-prompt.js +20 -12
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +4 -4
- package/dist/tools/fundamentals/company-overview.d.ts +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +1 -1
- package/dist/tools/fundamentals/financials.d.ts +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
- package/dist/tools/index.d.ts +28 -1
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.d.ts +1 -1
- package/dist/tools/interaction/twitter-login.d.ts +1 -1
- package/dist/tools/macro/fear-greed.d.ts +1 -1
- package/dist/tools/macro/fred-data.d.ts +1 -1
- package/dist/tools/market/crypto-history.d.ts +1 -1
- package/dist/tools/market/crypto-price.d.ts +1 -1
- package/dist/tools/market/search-ticker.d.ts +1 -1
- package/dist/tools/market/stock-history.d.ts +1 -1
- package/dist/tools/market/stock-quote.d.ts +1 -1
- package/dist/tools/options/option-chain.d.ts +1 -1
- package/dist/tools/options/option-chain.js +4 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/predictions.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/tracker.d.ts +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/web-search.d.ts +1 -1
- package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
- package/dist/tools/technical/backtest.d.ts +1 -1
- package/dist/tools/technical/indicators.d.ts +1 -1
- package/dist/tools/technical/indicators.js +7 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/workflows/options-screener.js +7 -2
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +3 -3
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/background-quotes.ts +31 -0
- package/gui/server/chat-event-adapter.ts +142 -0
- package/gui/server/invoke-tool.ts +89 -0
- package/gui/server/live-chat-event-adapter.ts +181 -0
- package/gui/server/model-setup.ts +100 -0
- package/gui/server/package.json +5 -0
- package/gui/server/projector.ts +212 -0
- package/gui/server/server.ts +592 -0
- package/gui/server/session-actions.ts +31 -0
- package/gui/server/tool-metadata.ts +88 -0
- package/gui/server/websocket.ts +128 -0
- package/gui/server/writer-lock.ts +118 -0
- package/gui/shared/chat-events.ts +118 -0
- package/gui/shared/event-reducer.ts +186 -0
- package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
- package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
- package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
- package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
- package/gui/web/dist/index.html +17 -0
- package/package.json +44 -18
- package/src/analysts/contracts.ts +189 -0
- package/src/analysts/orchestrator.ts +300 -0
- package/src/cli.ts +205 -0
- package/src/config.ts +161 -0
- package/src/index.ts +5 -0
- package/src/infra/browser.ts +111 -0
- package/src/infra/cache.ts +103 -0
- package/src/infra/http-client.ts +68 -0
- package/src/infra/index.ts +18 -0
- package/src/infra/native-dependencies.ts +12 -0
- package/src/infra/node-version.ts +24 -0
- package/src/infra/open-url.ts +28 -0
- package/src/infra/opencandle-paths.ts +64 -0
- package/src/infra/rate-limiter.ts +64 -0
- package/src/memory/index.ts +10 -0
- package/src/memory/manager.ts +159 -0
- package/src/memory/preference-extractor.ts +106 -0
- package/src/memory/retrieval.ts +70 -0
- package/src/memory/sqlite.ts +172 -0
- package/src/memory/storage.ts +204 -0
- package/src/memory/tool-defaults.ts +87 -0
- package/src/memory/types.ts +67 -0
- package/src/onboarding/connect.ts +184 -0
- package/src/onboarding/credential-interceptor.ts +134 -0
- package/src/onboarding/degradation-accumulator.ts +79 -0
- package/src/onboarding/prompt-user.ts +85 -0
- package/src/onboarding/providers.ts +315 -0
- package/src/onboarding/state.ts +218 -0
- package/src/onboarding/tool-helpers.ts +111 -0
- package/src/onboarding/tool-tags.ts +201 -0
- package/src/onboarding/validation.ts +158 -0
- package/src/pi/opencandle-extension.ts +724 -0
- package/src/pi/session-storage.ts +5 -0
- package/src/pi/session.ts +81 -0
- package/src/pi/setup.ts +371 -0
- package/src/pi/tool-adapter.ts +36 -0
- package/src/prompts/context-builder.ts +204 -0
- package/src/prompts/disclaimer.ts +9 -0
- package/src/prompts/sections.ts +46 -0
- package/src/prompts/workflow-prompts.ts +279 -0
- package/src/providers/alpha-vantage.ts +292 -0
- package/src/providers/coingecko.ts +96 -0
- package/src/providers/exa-search.ts +373 -0
- package/src/providers/fear-greed.ts +45 -0
- package/src/providers/finnhub.ts +124 -0
- package/src/providers/fred.ts +83 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider-credential-error.ts +23 -0
- package/src/providers/reddit.ts +151 -0
- package/src/providers/sec-edgar.ts +96 -0
- package/src/providers/twitter.ts +173 -0
- package/src/providers/web-search.ts +293 -0
- package/src/providers/with-fallback.ts +41 -0
- package/src/providers/wrap-provider.ts +64 -0
- package/src/providers/yahoo-finance.ts +367 -0
- package/src/routing/classify-intent.ts +194 -0
- package/src/routing/defaults.ts +29 -0
- package/src/routing/entity-extractor.ts +140 -0
- package/src/routing/index.ts +26 -0
- package/src/routing/router-llm-client.ts +51 -0
- package/src/routing/router-prompt.ts +159 -0
- package/src/routing/router-types.ts +66 -0
- package/src/routing/router.ts +213 -0
- package/src/routing/slot-resolver.ts +152 -0
- package/src/routing/types.ts +63 -0
- package/src/runtime/evidence.ts +77 -0
- package/src/runtime/index.ts +55 -0
- package/src/runtime/prompt-step.ts +75 -0
- package/src/runtime/provider-ids.ts +15 -0
- package/src/runtime/provider-tracker.ts +40 -0
- package/src/runtime/run-context.ts +22 -0
- package/src/runtime/session-coordinator.ts +406 -0
- package/src/runtime/tool-defaults-wrapper.ts +35 -0
- package/src/runtime/validation.ts +214 -0
- package/src/runtime/workflow-events.ts +75 -0
- package/src/runtime/workflow-runner.ts +188 -0
- package/src/runtime/workflow-types.ts +102 -0
- package/src/sentiment/adapters/finnhub.ts +44 -0
- package/src/sentiment/adapters/reddit.ts +65 -0
- package/src/sentiment/adapters/twitter.ts +36 -0
- package/src/sentiment/adapters/web.ts +44 -0
- package/src/sentiment/index.ts +58 -0
- package/src/sentiment/keywords.ts +9 -0
- package/src/sentiment/pipeline.ts +68 -0
- package/src/sentiment/scorer.ts +78 -0
- package/src/sentiment/store.ts +260 -0
- package/src/sentiment/trends.ts +90 -0
- package/src/sentiment/types.ts +108 -0
- package/src/system-prompt.ts +115 -0
- package/src/tool-kit.ts +68 -0
- package/src/tools/AGENTS.md +36 -0
- package/src/tools/fundamentals/company-overview.ts +54 -0
- package/src/tools/fundamentals/comps.ts +156 -0
- package/src/tools/fundamentals/dcf.ts +267 -0
- package/src/tools/fundamentals/earnings.ts +47 -0
- package/src/tools/fundamentals/financials.ts +54 -0
- package/src/tools/fundamentals/sec-filings.ts +61 -0
- package/src/tools/index.ts +88 -0
- package/src/tools/interaction/ask-user.ts +81 -0
- package/src/tools/interaction/twitter-login.ts +93 -0
- package/src/tools/macro/fear-greed.ts +41 -0
- package/src/tools/macro/fred-data.ts +54 -0
- package/src/tools/market/crypto-history.ts +51 -0
- package/src/tools/market/crypto-price.ts +53 -0
- package/src/tools/market/search-ticker.ts +53 -0
- package/src/tools/market/stock-history.ts +79 -0
- package/src/tools/market/stock-quote.ts +64 -0
- package/src/tools/options/greeks.ts +82 -0
- package/src/tools/options/option-chain.ts +91 -0
- package/src/tools/portfolio/correlation.ts +162 -0
- package/src/tools/portfolio/predictions.ts +253 -0
- package/src/tools/portfolio/risk-analysis.ts +134 -0
- package/src/tools/portfolio/tracker.ts +147 -0
- package/src/tools/portfolio/watchlist.ts +153 -0
- package/src/tools/sentiment/reddit-sentiment.ts +164 -0
- package/src/tools/sentiment/sentiment-summary.ts +256 -0
- package/src/tools/sentiment/sentiment-trend.ts +58 -0
- package/src/tools/sentiment/twitter-sentiment.ts +96 -0
- package/src/tools/sentiment/web-search.ts +150 -0
- package/src/tools/sentiment/web-sentiment.ts +76 -0
- package/src/tools/technical/backtest.ts +246 -0
- package/src/tools/technical/indicators.ts +258 -0
- package/src/types/fundamentals.ts +46 -0
- package/src/types/index.ts +20 -0
- package/src/types/macro.ts +27 -0
- package/src/types/market.ts +43 -0
- package/src/types/options.ts +35 -0
- package/src/types/portfolio.ts +41 -0
- package/src/types/sentiment.ts +70 -0
- package/src/workflows/compare-assets.ts +39 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/options-screener.ts +49 -0
- package/src/workflows/portfolio-builder.ts +52 -0
- package/src/workflows/types.ts +4 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
5
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
|
+
import { ensureParentDir, getPredictionsPath } from "../../infra/opencandle-paths.js";
|
|
7
|
+
|
|
8
|
+
export interface Prediction {
|
|
9
|
+
symbol: string;
|
|
10
|
+
direction: "bullish" | "bearish" | "neutral";
|
|
11
|
+
conviction: number; // 1-10
|
|
12
|
+
entryPrice: number;
|
|
13
|
+
targetPrice?: number;
|
|
14
|
+
date: string;
|
|
15
|
+
expiresAt: string;
|
|
16
|
+
timeframeDays: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PredictionCheckResult {
|
|
20
|
+
total: number;
|
|
21
|
+
open: number;
|
|
22
|
+
correct: number;
|
|
23
|
+
wrong: number;
|
|
24
|
+
hitRate: number;
|
|
25
|
+
weightedHitRate: number;
|
|
26
|
+
details: Array<{
|
|
27
|
+
symbol: string;
|
|
28
|
+
direction: string;
|
|
29
|
+
conviction: number;
|
|
30
|
+
entryPrice: number;
|
|
31
|
+
currentPrice: number;
|
|
32
|
+
pnlPercent: number;
|
|
33
|
+
correct: boolean;
|
|
34
|
+
status: "open" | "resolved";
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadPredictions(): Prediction[] {
|
|
39
|
+
const predictionsPath = getPredictionsPath();
|
|
40
|
+
if (!existsSync(predictionsPath)) return [];
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(readFileSync(predictionsPath, "utf-8"));
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function savePredictions(predictions: Prediction[]): void {
|
|
49
|
+
const predictionsPath = getPredictionsPath();
|
|
50
|
+
ensureParentDir(predictionsPath);
|
|
51
|
+
writeFileSync(predictionsPath, JSON.stringify(predictions, null, 2));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function recordPrediction(params: {
|
|
55
|
+
symbol: string;
|
|
56
|
+
direction: "bullish" | "bearish" | "neutral";
|
|
57
|
+
conviction: number;
|
|
58
|
+
entryPrice: number;
|
|
59
|
+
targetPrice?: number;
|
|
60
|
+
timeframeDays: number;
|
|
61
|
+
}): Prediction {
|
|
62
|
+
const predictions = loadPredictions();
|
|
63
|
+
const now = new Date();
|
|
64
|
+
const expires = new Date(now);
|
|
65
|
+
expires.setDate(expires.getDate() + params.timeframeDays);
|
|
66
|
+
|
|
67
|
+
const prediction: Prediction = {
|
|
68
|
+
symbol: params.symbol.toUpperCase(),
|
|
69
|
+
direction: params.direction,
|
|
70
|
+
conviction: params.conviction,
|
|
71
|
+
entryPrice: params.entryPrice,
|
|
72
|
+
targetPrice: params.targetPrice,
|
|
73
|
+
date: now.toISOString().split("T")[0],
|
|
74
|
+
expiresAt: expires.toISOString().split("T")[0],
|
|
75
|
+
timeframeDays: params.timeframeDays,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
predictions.push(prediction);
|
|
79
|
+
savePredictions(predictions);
|
|
80
|
+
return prediction;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function checkPredictions(
|
|
84
|
+
predictions: Prediction[],
|
|
85
|
+
currentPrices: Map<string, number>,
|
|
86
|
+
now: Date = new Date(),
|
|
87
|
+
): PredictionCheckResult {
|
|
88
|
+
if (predictions.length === 0) {
|
|
89
|
+
return { total: 0, open: 0, correct: 0, wrong: 0, hitRate: 0, weightedHitRate: 0, details: [] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const details: PredictionCheckResult["details"] = [];
|
|
93
|
+
let totalConviction = 0;
|
|
94
|
+
let correctConviction = 0;
|
|
95
|
+
let openCount = 0;
|
|
96
|
+
|
|
97
|
+
const nowStr = now.toISOString().split("T")[0];
|
|
98
|
+
|
|
99
|
+
for (const p of predictions) {
|
|
100
|
+
const currentPrice = currentPrices.get(p.symbol);
|
|
101
|
+
if (currentPrice == null) continue;
|
|
102
|
+
|
|
103
|
+
const isExpired = p.expiresAt <= nowStr;
|
|
104
|
+
const pnlPercent = (currentPrice - p.entryPrice) / p.entryPrice;
|
|
105
|
+
|
|
106
|
+
if (!isExpired) {
|
|
107
|
+
openCount++;
|
|
108
|
+
details.push({
|
|
109
|
+
symbol: p.symbol,
|
|
110
|
+
direction: p.direction,
|
|
111
|
+
conviction: p.conviction,
|
|
112
|
+
entryPrice: p.entryPrice,
|
|
113
|
+
currentPrice,
|
|
114
|
+
pnlPercent,
|
|
115
|
+
correct: false,
|
|
116
|
+
status: "open",
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const correct =
|
|
122
|
+
(p.direction === "bullish" && currentPrice > p.entryPrice) ||
|
|
123
|
+
(p.direction === "bearish" && currentPrice < p.entryPrice) ||
|
|
124
|
+
(p.direction === "neutral" && Math.abs(pnlPercent) < 0.02);
|
|
125
|
+
|
|
126
|
+
details.push({
|
|
127
|
+
symbol: p.symbol,
|
|
128
|
+
direction: p.direction,
|
|
129
|
+
conviction: p.conviction,
|
|
130
|
+
entryPrice: p.entryPrice,
|
|
131
|
+
currentPrice,
|
|
132
|
+
pnlPercent,
|
|
133
|
+
correct,
|
|
134
|
+
status: "resolved",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
totalConviction += p.conviction;
|
|
138
|
+
if (correct) correctConviction += p.conviction;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const resolved = details.filter((d) => d.status === "resolved");
|
|
142
|
+
const correctCount = resolved.filter((d) => d.correct).length;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
total: details.length,
|
|
146
|
+
open: openCount,
|
|
147
|
+
correct: correctCount,
|
|
148
|
+
wrong: resolved.length - correctCount,
|
|
149
|
+
hitRate: resolved.length > 0 ? correctCount / resolved.length : 0,
|
|
150
|
+
weightedHitRate: totalConviction > 0 ? correctConviction / totalConviction : 0,
|
|
151
|
+
details,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const params = Type.Object({
|
|
156
|
+
action: Type.Union(
|
|
157
|
+
[Type.Literal("record"), Type.Literal("check")],
|
|
158
|
+
{ description: "record: save a new prediction. check: evaluate all predictions against current prices." },
|
|
159
|
+
),
|
|
160
|
+
symbol: Type.Optional(Type.String({ description: "Ticker symbol (required for record)" })),
|
|
161
|
+
direction: Type.Optional(
|
|
162
|
+
Type.Union(
|
|
163
|
+
[Type.Literal("bullish"), Type.Literal("bearish"), Type.Literal("neutral")],
|
|
164
|
+
{ description: "Predicted direction (required for record)" },
|
|
165
|
+
),
|
|
166
|
+
),
|
|
167
|
+
conviction: Type.Optional(
|
|
168
|
+
Type.Number({ description: "Conviction 1-10 (required for record)" }),
|
|
169
|
+
),
|
|
170
|
+
entry_price: Type.Optional(
|
|
171
|
+
Type.Number({ description: "Entry price at time of prediction (required for record)" }),
|
|
172
|
+
),
|
|
173
|
+
target_price: Type.Optional(
|
|
174
|
+
Type.Number({ description: "Optional target price" }),
|
|
175
|
+
),
|
|
176
|
+
timeframe_days: Type.Optional(
|
|
177
|
+
Type.Number({ description: "Timeframe in days for the prediction (default: 30)" }),
|
|
178
|
+
),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
export const predictionsTool: AgentTool<typeof params> = {
|
|
182
|
+
name: "track_prediction",
|
|
183
|
+
label: "Prediction Tracker",
|
|
184
|
+
description:
|
|
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
|
+
parameters: params,
|
|
187
|
+
async execute(toolCallId, args) {
|
|
188
|
+
if (args.action === "record") {
|
|
189
|
+
if (!args.symbol || !args.direction || !args.conviction || !args.entry_price) {
|
|
190
|
+
throw new Error("symbol, direction, conviction, and entry_price are required for record action.");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const prediction = recordPrediction({
|
|
194
|
+
symbol: args.symbol,
|
|
195
|
+
direction: args.direction,
|
|
196
|
+
conviction: args.conviction,
|
|
197
|
+
entryPrice: args.entry_price,
|
|
198
|
+
targetPrice: args.target_price,
|
|
199
|
+
timeframeDays: args.timeframe_days ?? 30,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [{ type: "text", text: `Recorded: ${prediction.symbol} ${prediction.direction} (conviction ${prediction.conviction}/10) at $${prediction.entryPrice}. Expires ${prediction.expiresAt}.` }],
|
|
204
|
+
details: prediction,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check action
|
|
209
|
+
const predictions = loadPredictions();
|
|
210
|
+
if (predictions.length === 0) {
|
|
211
|
+
return {
|
|
212
|
+
content: [{ type: "text", text: "No predictions recorded yet. Use record action to track your calls." }],
|
|
213
|
+
details: null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Fetch current prices for all symbols
|
|
218
|
+
const symbols = [...new Set(predictions.map((p) => p.symbol))];
|
|
219
|
+
const priceMap = new Map<string, number>();
|
|
220
|
+
await Promise.all(
|
|
221
|
+
symbols.map(async (sym) => {
|
|
222
|
+
const result = await wrapProvider("yahoo", () => getQuote(sym));
|
|
223
|
+
if (result.status === "ok") {
|
|
224
|
+
priceMap.set(sym, result.data.price);
|
|
225
|
+
} else {
|
|
226
|
+
// Skip symbols that are unavailable
|
|
227
|
+
}
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const result = checkPredictions(predictions, priceMap);
|
|
232
|
+
|
|
233
|
+
const resolved = result.correct + result.wrong;
|
|
234
|
+
const lines = [
|
|
235
|
+
`**Prediction Scorecard** — ${result.total} predictions (${resolved} resolved, ${result.open} open)`,
|
|
236
|
+
``,
|
|
237
|
+
`Hit Rate: ${(result.hitRate * 100).toFixed(0)}% (${result.correct}/${resolved})`,
|
|
238
|
+
`Weighted Hit Rate: ${(result.weightedHitRate * 100).toFixed(0)}% (by conviction)`,
|
|
239
|
+
``,
|
|
240
|
+
...result.details.map((d) => {
|
|
241
|
+
const icon = d.status === "open" ? "~" : d.correct ? "+" : "-";
|
|
242
|
+
const sign = d.pnlPercent >= 0 ? "+" : "";
|
|
243
|
+
const label = d.status === "open" ? " (open)" : "";
|
|
244
|
+
return ` [${icon}] ${d.symbol}: ${d.direction} (conv ${d.conviction}) → $${d.entryPrice.toFixed(2)} → $${d.currentPrice.toFixed(2)} (${sign}${(d.pnlPercent * 100).toFixed(1)}%)${label}`;
|
|
245
|
+
}),
|
|
246
|
+
];
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
250
|
+
details: result,
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { getHistory } from "../../providers/yahoo-finance.js";
|
|
4
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
|
+
import type { RiskMetrics } from "../../types/portfolio.js";
|
|
6
|
+
|
|
7
|
+
const params = Type.Object({
|
|
8
|
+
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT, SPY)" }),
|
|
9
|
+
period: Type.Optional(
|
|
10
|
+
Type.String({ description: "Historical period for analysis: 6mo, 1y, 2y. Default: 1y" }),
|
|
11
|
+
),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const riskAnalysisTool: AgentTool<typeof params, RiskMetrics> = {
|
|
15
|
+
name: "analyze_risk",
|
|
16
|
+
label: "Risk Analysis",
|
|
17
|
+
description:
|
|
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
|
+
parameters: params,
|
|
20
|
+
async execute(toolCallId, args) {
|
|
21
|
+
const symbol = args.symbol.toUpperCase();
|
|
22
|
+
const period = args.period ?? "1y";
|
|
23
|
+
const result = await wrapProvider("yahoo", () => getHistory(symbol, period, "1d"));
|
|
24
|
+
if (result.status === "unavailable") {
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text: `⚠ Risk analysis unavailable for ${symbol} (${result.reason}).` }],
|
|
27
|
+
details: null as any,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const bars = result.data;
|
|
31
|
+
const closes = bars.map((b) => b.close);
|
|
32
|
+
|
|
33
|
+
if (closes.length < 30) {
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: `Insufficient data for risk analysis (need 30+ days, got ${closes.length})` }],
|
|
36
|
+
details: null as any,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const metrics = computeRiskMetrics(symbol, closes);
|
|
41
|
+
|
|
42
|
+
const text = [
|
|
43
|
+
`**${symbol} Risk Analysis** (${bars[0].date} to ${bars[bars.length - 1].date}, ${closes.length} days)`,
|
|
44
|
+
``,
|
|
45
|
+
`Annualized Return: ${(metrics.annualizedReturn * 100).toFixed(2)}%`,
|
|
46
|
+
`Annualized Volatility: ${(metrics.annualizedVolatility * 100).toFixed(2)}%`,
|
|
47
|
+
`Sharpe Ratio: ${metrics.sharpeRatio.toFixed(2)} ${sharpeLabel(metrics.sharpeRatio)}`,
|
|
48
|
+
`Max Drawdown: ${(metrics.maxDrawdown * 100).toFixed(2)}%`,
|
|
49
|
+
`Value at Risk (95%, daily): ${(metrics.var95 * 100).toFixed(2)}%`,
|
|
50
|
+
``,
|
|
51
|
+
riskSummary(metrics),
|
|
52
|
+
].join("\n");
|
|
53
|
+
|
|
54
|
+
return { content: [{ type: "text", text }], details: metrics };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function computeRiskMetrics(symbol: string, closes: number[]): RiskMetrics {
|
|
59
|
+
const dailyReturns = computeDailyReturns(closes);
|
|
60
|
+
const avgDailyReturn = mean(dailyReturns);
|
|
61
|
+
const dailyVol = stddev(dailyReturns);
|
|
62
|
+
|
|
63
|
+
const annualizedReturn = avgDailyReturn * 252;
|
|
64
|
+
const annualizedVolatility = dailyVol * Math.sqrt(252);
|
|
65
|
+
|
|
66
|
+
// Sharpe ratio (assuming 5% risk-free rate)
|
|
67
|
+
const riskFreeDaily = 0.05 / 252;
|
|
68
|
+
const sharpeRatio =
|
|
69
|
+
dailyVol === 0 ? 0 : ((avgDailyReturn - riskFreeDaily) / dailyVol) * Math.sqrt(252);
|
|
70
|
+
|
|
71
|
+
const maxDrawdown = computeMaxDrawdown(closes);
|
|
72
|
+
const var95 = computeVaR(dailyReturns, 0.05);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
symbol,
|
|
76
|
+
annualizedReturn,
|
|
77
|
+
annualizedVolatility,
|
|
78
|
+
sharpeRatio,
|
|
79
|
+
maxDrawdown,
|
|
80
|
+
var95,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function computeDailyReturns(prices: number[]): number[] {
|
|
85
|
+
const returns: number[] = [];
|
|
86
|
+
for (let i = 1; i < prices.length; i++) {
|
|
87
|
+
returns.push((prices[i] - prices[i - 1]) / prices[i - 1]);
|
|
88
|
+
}
|
|
89
|
+
return returns;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function computeMaxDrawdown(prices: number[]): number {
|
|
93
|
+
let peak = prices[0];
|
|
94
|
+
let maxDd = 0;
|
|
95
|
+
for (const price of prices) {
|
|
96
|
+
if (price > peak) peak = price;
|
|
97
|
+
const dd = (peak - price) / peak;
|
|
98
|
+
if (dd > maxDd) maxDd = dd;
|
|
99
|
+
}
|
|
100
|
+
return maxDd;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function computeVaR(returns: number[], confidence: number): number {
|
|
104
|
+
const sorted = [...returns].sort((a, b) => a - b);
|
|
105
|
+
const idx = Math.floor(sorted.length * confidence);
|
|
106
|
+
return Math.abs(sorted[idx]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mean(arr: number[]): number {
|
|
110
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stddev(arr: number[]): number {
|
|
114
|
+
const m = mean(arr);
|
|
115
|
+
const variance = arr.reduce((sum, val) => sum + (val - m) ** 2, 0) / arr.length;
|
|
116
|
+
return Math.sqrt(variance);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sharpeLabel(s: number): string {
|
|
120
|
+
if (s >= 2) return "(Excellent)";
|
|
121
|
+
if (s >= 1) return "(Good)";
|
|
122
|
+
if (s >= 0) return "(Below average)";
|
|
123
|
+
return "(Negative — losing money)";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function riskSummary(m: RiskMetrics): string {
|
|
127
|
+
const signals: string[] = [];
|
|
128
|
+
if (m.annualizedVolatility > 0.4) signals.push("High volatility stock");
|
|
129
|
+
if (m.maxDrawdown > 0.3) signals.push("Large historical drawdown (>30%)");
|
|
130
|
+
if (m.sharpeRatio < 0) signals.push("Negative risk-adjusted returns");
|
|
131
|
+
if (m.sharpeRatio >= 1.5) signals.push("Strong risk-adjusted performance");
|
|
132
|
+
if (m.var95 > 0.03) signals.push("High daily VaR (>3%)");
|
|
133
|
+
return signals.length > 0 ? "Flags: " + signals.join(" | ") : "Risk profile appears moderate";
|
|
134
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
5
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
|
+
import type { Position, PortfolioSummary } from "../../types/portfolio.js";
|
|
7
|
+
import { ensureParentDir, getPortfolioPath } from "../../infra/opencandle-paths.js";
|
|
8
|
+
|
|
9
|
+
function loadPortfolio(): Position[] {
|
|
10
|
+
const portfolioPath = getPortfolioPath();
|
|
11
|
+
if (!existsSync(portfolioPath)) return [];
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(portfolioPath, "utf-8"));
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function savePortfolio(positions: Position[]): void {
|
|
20
|
+
const portfolioPath = getPortfolioPath();
|
|
21
|
+
ensureParentDir(portfolioPath);
|
|
22
|
+
writeFileSync(portfolioPath, JSON.stringify(positions, null, 2));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getCurrentPrice(symbol: string): Promise<number | null> {
|
|
26
|
+
const result = await wrapProvider("yahoo", () => getQuote(symbol));
|
|
27
|
+
if (result.status === "unavailable") return null;
|
|
28
|
+
return result.data.price;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const params = Type.Object({
|
|
32
|
+
action: Type.Union([
|
|
33
|
+
Type.Literal("add"),
|
|
34
|
+
Type.Literal("remove"),
|
|
35
|
+
Type.Literal("view"),
|
|
36
|
+
], { description: "Action: add a position, remove a position, or view portfolio" }),
|
|
37
|
+
symbol: Type.Optional(
|
|
38
|
+
Type.String({ description: "Ticker symbol — stocks (AAPL, MSFT) or crypto with -USD suffix (BTC-USD, ETH-USD, SOL-USD). Use search_ticker to find the right ticker." }),
|
|
39
|
+
),
|
|
40
|
+
shares: Type.Optional(
|
|
41
|
+
Type.Number({ description: "Number of shares/units (required for add)" }),
|
|
42
|
+
),
|
|
43
|
+
avg_cost: Type.Optional(
|
|
44
|
+
Type.Number({ description: "Average cost per share/unit in USD (required for add)" }),
|
|
45
|
+
),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const portfolioTrackerTool: AgentTool<typeof params, PortfolioSummary | null> = {
|
|
49
|
+
name: "track_portfolio",
|
|
50
|
+
label: "Portfolio Tracker",
|
|
51
|
+
description:
|
|
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
|
+
parameters: params,
|
|
54
|
+
async execute(toolCallId, args) {
|
|
55
|
+
const positions = loadPortfolio();
|
|
56
|
+
|
|
57
|
+
if (args.action === "add") {
|
|
58
|
+
if (!args.symbol || !args.shares || !args.avg_cost) {
|
|
59
|
+
throw new Error("symbol, shares, and avg_cost are required for add action.");
|
|
60
|
+
}
|
|
61
|
+
const symbol = args.symbol.toUpperCase();
|
|
62
|
+
const existing = positions.find((p) => p.symbol === symbol);
|
|
63
|
+
if (existing) {
|
|
64
|
+
const totalShares = existing.shares + args.shares;
|
|
65
|
+
existing.avgCost =
|
|
66
|
+
(existing.avgCost * existing.shares + args.avg_cost * args.shares) / totalShares;
|
|
67
|
+
existing.shares = totalShares;
|
|
68
|
+
} else {
|
|
69
|
+
positions.push({
|
|
70
|
+
symbol,
|
|
71
|
+
shares: args.shares,
|
|
72
|
+
avgCost: args.avg_cost,
|
|
73
|
+
addedAt: new Date().toISOString(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
savePortfolio(positions);
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text: `Added ${args.shares} shares of ${symbol} at $${args.avg_cost.toFixed(2)}` }],
|
|
79
|
+
details: null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (args.action === "remove") {
|
|
84
|
+
if (!args.symbol) {
|
|
85
|
+
throw new Error("symbol is required for remove action.");
|
|
86
|
+
}
|
|
87
|
+
const symbol = args.symbol.toUpperCase();
|
|
88
|
+
const idx = positions.findIndex((p) => p.symbol === symbol);
|
|
89
|
+
if (idx === -1) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: `${symbol} not found in portfolio` }],
|
|
92
|
+
details: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
positions.splice(idx, 1);
|
|
96
|
+
savePortfolio(positions);
|
|
97
|
+
return {
|
|
98
|
+
content: [{ type: "text", text: `Removed ${symbol} from portfolio` }],
|
|
99
|
+
details: null,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// View portfolio
|
|
104
|
+
if (positions.length === 0) {
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: "Portfolio is empty. Use add action to add positions." }],
|
|
107
|
+
details: null,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const enriched = await Promise.all(
|
|
112
|
+
positions.map(async (p) => {
|
|
113
|
+
const currentPrice = await getCurrentPrice(p.symbol) ?? p.avgCost;
|
|
114
|
+
const marketValue = currentPrice * p.shares;
|
|
115
|
+
const totalCost = p.avgCost * p.shares;
|
|
116
|
+
return {
|
|
117
|
+
...p,
|
|
118
|
+
currentPrice,
|
|
119
|
+
marketValue,
|
|
120
|
+
totalCost,
|
|
121
|
+
pnl: marketValue - totalCost,
|
|
122
|
+
pnlPercent: ((marketValue - totalCost) / totalCost) * 100,
|
|
123
|
+
};
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const totalValue = enriched.reduce((s, p) => s + p.marketValue, 0);
|
|
128
|
+
const totalCost = enriched.reduce((s, p) => s + p.totalCost, 0);
|
|
129
|
+
|
|
130
|
+
const summary: PortfolioSummary = {
|
|
131
|
+
positions: enriched,
|
|
132
|
+
totalValue,
|
|
133
|
+
totalCost,
|
|
134
|
+
totalPnl: totalValue - totalCost,
|
|
135
|
+
totalPnlPercent: totalCost > 0 ? ((totalValue - totalCost) / totalCost) * 100 : 0,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const header = `**Portfolio** — ${enriched.length} positions | Value: $${totalValue.toFixed(2)} | P&L: $${summary.totalPnl.toFixed(2)} (${summary.totalPnlPercent >= 0 ? "+" : ""}${summary.totalPnlPercent.toFixed(2)}%)`;
|
|
139
|
+
const rows = enriched.map((p) => {
|
|
140
|
+
const sign = p.pnlPercent >= 0 ? "+" : "";
|
|
141
|
+
return ` ${p.symbol}: ${p.shares} @ $${p.avgCost.toFixed(2)} → $${p.currentPrice.toFixed(2)} | P&L: $${p.pnl.toFixed(2)} (${sign}${p.pnlPercent.toFixed(2)}%)`;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const text = [header, ...rows].join("\n");
|
|
145
|
+
return { content: [{ type: "text", text }], details: summary };
|
|
146
|
+
},
|
|
147
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
5
|
+
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
|
+
import { ensureParentDir, getWatchlistPath } from "../../infra/opencandle-paths.js";
|
|
7
|
+
|
|
8
|
+
interface WatchlistItem {
|
|
9
|
+
symbol: string;
|
|
10
|
+
addedAt: string;
|
|
11
|
+
targetPrice?: number;
|
|
12
|
+
stopPrice?: number;
|
|
13
|
+
notes?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function loadWatchlist(): WatchlistItem[] {
|
|
17
|
+
const watchlistPath = getWatchlistPath();
|
|
18
|
+
if (!existsSync(watchlistPath)) return [];
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(readFileSync(watchlistPath, "utf-8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveWatchlist(items: WatchlistItem[]): void {
|
|
27
|
+
const watchlistPath = getWatchlistPath();
|
|
28
|
+
ensureParentDir(watchlistPath);
|
|
29
|
+
writeFileSync(watchlistPath, JSON.stringify(items, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const params = Type.Object({
|
|
33
|
+
action: Type.Union(
|
|
34
|
+
[Type.Literal("add"), Type.Literal("remove"), Type.Literal("check")],
|
|
35
|
+
{ description: "One of: 'add', 'remove', or 'check'" },
|
|
36
|
+
),
|
|
37
|
+
symbol: Type.Optional(
|
|
38
|
+
Type.String({ description: "Ticker symbol (required for add/remove)" }),
|
|
39
|
+
),
|
|
40
|
+
target_price: Type.Optional(
|
|
41
|
+
Type.Number({ description: "Alert when price rises above this level" }),
|
|
42
|
+
),
|
|
43
|
+
stop_price: Type.Optional(
|
|
44
|
+
Type.Number({ description: "Alert when price falls below this level" }),
|
|
45
|
+
),
|
|
46
|
+
notes: Type.Optional(
|
|
47
|
+
Type.String({ description: "Optional notes for why you're watching this" }),
|
|
48
|
+
),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export const watchlistTool: AgentTool<typeof params> = {
|
|
52
|
+
name: "manage_watchlist",
|
|
53
|
+
label: "Watchlist",
|
|
54
|
+
description:
|
|
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
|
+
parameters: params,
|
|
57
|
+
async execute(toolCallId, args) {
|
|
58
|
+
const items = loadWatchlist();
|
|
59
|
+
|
|
60
|
+
if (args.action === "add") {
|
|
61
|
+
if (!args.symbol) {
|
|
62
|
+
throw new Error("symbol is required for add action.");
|
|
63
|
+
}
|
|
64
|
+
const symbol = args.symbol.toUpperCase();
|
|
65
|
+
const existing = items.findIndex((i) => i.symbol === symbol);
|
|
66
|
+
const item: WatchlistItem = {
|
|
67
|
+
symbol,
|
|
68
|
+
addedAt: new Date().toISOString(),
|
|
69
|
+
...(args.target_price != null && { targetPrice: args.target_price }),
|
|
70
|
+
...(args.stop_price != null && { stopPrice: args.stop_price }),
|
|
71
|
+
...(args.notes != null && { notes: args.notes }),
|
|
72
|
+
};
|
|
73
|
+
if (existing >= 0) {
|
|
74
|
+
items[existing] = item;
|
|
75
|
+
} else {
|
|
76
|
+
items.push(item);
|
|
77
|
+
}
|
|
78
|
+
saveWatchlist(items);
|
|
79
|
+
const alerts = [];
|
|
80
|
+
if (args.target_price) alerts.push(`target: $${args.target_price}`);
|
|
81
|
+
if (args.stop_price) alerts.push(`stop: $${args.stop_price}`);
|
|
82
|
+
const alertStr = alerts.length > 0 ? ` (${alerts.join(", ")})` : "";
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: `Added ${symbol} to watchlist${alertStr}` }],
|
|
85
|
+
details: null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (args.action === "remove") {
|
|
90
|
+
if (!args.symbol) {
|
|
91
|
+
throw new Error("symbol is required for remove action.");
|
|
92
|
+
}
|
|
93
|
+
const symbol = args.symbol.toUpperCase();
|
|
94
|
+
const idx = items.findIndex((i) => i.symbol === symbol);
|
|
95
|
+
if (idx === -1) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: "text", text: `${symbol} not found in watchlist` }],
|
|
98
|
+
details: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
items.splice(idx, 1);
|
|
102
|
+
saveWatchlist(items);
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: `Removed ${symbol} from watchlist` }],
|
|
105
|
+
details: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check action
|
|
110
|
+
if (items.length === 0) {
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: "text", text: "Watchlist is empty. Use add action to add symbols." }],
|
|
113
|
+
details: null,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const checks = await Promise.all(
|
|
118
|
+
items.map(async (item) => {
|
|
119
|
+
const result = await wrapProvider("yahoo", () => getQuote(item.symbol));
|
|
120
|
+
if (result.status === "unavailable") {
|
|
121
|
+
return { ...item, currentPrice: 0, alerts: [`UNAVAILABLE: ${result.reason}`] };
|
|
122
|
+
}
|
|
123
|
+
const quote = result.data;
|
|
124
|
+
const alerts: string[] = [];
|
|
125
|
+
if (item.targetPrice && quote.price >= item.targetPrice) {
|
|
126
|
+
alerts.push(`TARGET HIT: $${quote.price.toFixed(2)} >= $${item.targetPrice}`);
|
|
127
|
+
}
|
|
128
|
+
if (item.stopPrice && quote.price <= item.stopPrice) {
|
|
129
|
+
alerts.push(`STOP ALERT: $${quote.price.toFixed(2)} fell below $${item.stopPrice}`);
|
|
130
|
+
}
|
|
131
|
+
return { ...item, currentPrice: quote.price, alerts };
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const alertItems = checks.filter((c) => c.alerts.length > 0);
|
|
136
|
+
const lines = [
|
|
137
|
+
`**Watchlist** — ${items.length} symbols${alertItems.length > 0 ? ` | ${alertItems.length} ALERT(S)` : ""}`,
|
|
138
|
+
"",
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
for (const c of checks) {
|
|
142
|
+
const alertStr = c.alerts.length > 0 ? ` ** ${c.alerts.join(" | ")} **` : "";
|
|
143
|
+
const targetStr = c.targetPrice ? ` | Target: $${c.targetPrice}` : "";
|
|
144
|
+
const stopStr = c.stopPrice ? ` | Stop: $${c.stopPrice}` : "";
|
|
145
|
+
lines.push(` ${c.symbol}: $${c.currentPrice.toFixed(2)}${targetStr}${stopStr}${alertStr}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
150
|
+
details: { items: checks },
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|