opencandle 0.12.0 → 0.13.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/README.md +48 -180
- package/dist/cli-main.js +9 -8
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/doctor/cli-command.js +7 -4
- package/dist/doctor/report.js +13 -0
- package/dist/gui/server/chat-event-adapter.js +145 -20
- package/dist/gui/server/history-snapshot-store.d.ts +8 -0
- package/dist/gui/server/history-snapshot-store.js +43 -0
- package/dist/gui/server/http-routes.d.ts +2 -0
- package/dist/gui/server/http-routes.js +32 -7
- package/dist/gui/server/live-chat-event-adapter.d.ts +2 -0
- package/dist/gui/server/live-chat-event-adapter.js +33 -15
- package/dist/gui/server/market-indices-api.d.ts +21 -0
- package/dist/gui/server/market-indices-api.js +26 -0
- package/dist/gui/server/market-indices-snapshot-store.d.ts +12 -0
- package/dist/gui/server/market-indices-snapshot-store.js +56 -0
- package/dist/gui/server/market-state-api.d.ts +112 -0
- package/dist/gui/server/market-state-api.js +246 -4
- package/dist/gui/server/model-setup.d.ts +2 -11
- package/dist/gui/server/model-setup.js +10 -6
- package/dist/gui/server/server.js +12 -9
- package/dist/gui/server/tool-metadata.d.ts +14 -14
- package/dist/gui/server/ws-hub.js +1 -1
- package/dist/infra/cache.d.ts +4 -0
- package/dist/infra/cache.js +4 -0
- package/dist/infra/lse-byte-budget.d.ts +10 -0
- package/dist/infra/lse-byte-budget.js +47 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/onboarding/providers.d.ts +16 -1
- package/dist/onboarding/providers.js +20 -0
- package/dist/onboarding/validate-model-key.js +33 -1
- package/dist/onboarding/validation.js +8 -0
- package/dist/pi/opencandle-extension.d.ts +2 -1
- package/dist/pi/opencandle-extension.js +2 -2
- package/dist/pi/session.d.ts +2 -3
- package/dist/pi/session.js +7 -5
- package/dist/pi/setup.d.ts +3 -3
- package/dist/pi/setup.js +51 -59
- package/dist/prompts/workflow-prompts.js +16 -14
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/lse.d.ts +35 -0
- package/dist/providers/lse.js +284 -0
- package/dist/providers/polymarket.js +22 -5
- package/dist/providers/wrap-provider.js +1 -0
- package/dist/providers/yahoo-finance.js +1 -0
- package/dist/routing/route-manifest.js +1 -0
- package/dist/routing/router.js +8 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js +1 -1
- package/dist/runtime/session-coordinator.d.ts +2 -2
- package/dist/runtime/session-coordinator.js +2 -2
- package/dist/tools/fundamentals/dcf.js +37 -8
- package/dist/tools/fundamentals/financials.js +88 -10
- package/dist/tools/index.d.ts +10 -5
- package/dist/tools/index.js +3 -0
- package/dist/tools/market/price-comparison.d.ts +30 -0
- package/dist/tools/market/price-comparison.js +202 -0
- package/dist/tools/market/stock-history.d.ts +18 -3
- package/dist/tools/market/stock-history.js +132 -17
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/types/market.d.ts +2 -0
- package/gui/web/dist/assets/CatalogOverlay-Cy8Fq0fn.js +1 -0
- package/gui/web/dist/assets/allocation-donut-DgBRIKCk.js +52 -0
- package/gui/web/dist/assets/index-DIlFyIOO.js +66 -0
- package/gui/web/dist/assets/index-DS4jESOB.css +2 -0
- package/gui/web/dist/assets/market-chart-DammaCjH.js +1 -0
- package/gui/web/dist/assets/utils-CnADgYgh.js +1 -0
- package/gui/web/dist/index.html +3 -2
- package/package.json +15 -17
- package/gui/web/dist/assets/CatalogOverlay-CAc7e3Pf.js +0 -1
- package/gui/web/dist/assets/index-BOEKd9wT.css +0 -2
- package/gui/web/dist/assets/index-C-H05cQ2.js +0 -65
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { getConfig } from "../config.js";
|
|
2
|
+
import { cache, STALE_LIMIT, TTL } from "../infra/cache.js";
|
|
3
|
+
import { markExhausted, recordBytes } from "../infra/lse-byte-budget.js";
|
|
4
|
+
import { rateLimiter } from "../infra/rate-limiter.js";
|
|
5
|
+
import { ProviderCredentialError } from "./provider-credential-error.js";
|
|
6
|
+
const BASE_URL = "https://api.londonstrategicedge.com/vault";
|
|
7
|
+
const MAX_RETRIES = 2;
|
|
8
|
+
const RETRY_DELAY_MS = 1_000;
|
|
9
|
+
const MAX_RETRY_AFTER_MS = 5_000;
|
|
10
|
+
export class LseHttpError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
detail;
|
|
13
|
+
constructor(status, detail) {
|
|
14
|
+
super(`LSE HTTP ${status}: ${detail}`);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.detail = detail;
|
|
17
|
+
this.name = "LseHttpError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function throwIfLseAuthError(error) {
|
|
21
|
+
if (error instanceof ProviderCredentialError && error.provider === "lse")
|
|
22
|
+
throw error;
|
|
23
|
+
if (error instanceof LseHttpError && (error.status === 401 || error.status === 403)) {
|
|
24
|
+
throw new ProviderCredentialError("lse", "stale", error.status);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function readErrorDetail(response) {
|
|
28
|
+
try {
|
|
29
|
+
const body = (await response.json());
|
|
30
|
+
if (typeof body.detail === "string")
|
|
31
|
+
return body.detail;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Preserve a useful typed HTTP error even when the provider body is not JSON.
|
|
35
|
+
}
|
|
36
|
+
return response.statusText || `HTTP ${response.status}`;
|
|
37
|
+
}
|
|
38
|
+
function parseRetryAfterMs(value) {
|
|
39
|
+
if (!value?.trim())
|
|
40
|
+
return undefined;
|
|
41
|
+
const seconds = Number(value.trim());
|
|
42
|
+
if (Number.isFinite(seconds) && seconds >= 0)
|
|
43
|
+
return seconds * 1_000;
|
|
44
|
+
const dateMs = Date.parse(value.trim());
|
|
45
|
+
if (Number.isNaN(dateMs))
|
|
46
|
+
return undefined;
|
|
47
|
+
return Math.max(0, dateMs - Date.now());
|
|
48
|
+
}
|
|
49
|
+
function sleep(ms) {
|
|
50
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
}
|
|
52
|
+
async function lseGet(path, params) {
|
|
53
|
+
const apiKey = getConfig().lseApiKey;
|
|
54
|
+
if (!apiKey)
|
|
55
|
+
throw new ProviderCredentialError("lse", "missing");
|
|
56
|
+
const url = `${BASE_URL}${path}?${new URLSearchParams(params)}`;
|
|
57
|
+
let lastError;
|
|
58
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
59
|
+
const response = await globalThis.fetch(url, { headers: { "x-api-key": apiKey } });
|
|
60
|
+
const dataBytes = response.headers.get("x-data-bytes");
|
|
61
|
+
if (dataBytes !== null) {
|
|
62
|
+
const parsedBytes = Number(dataBytes);
|
|
63
|
+
if (Number.isFinite(parsedBytes))
|
|
64
|
+
recordBytes(parsedBytes);
|
|
65
|
+
}
|
|
66
|
+
if (response.ok)
|
|
67
|
+
return (await response.json());
|
|
68
|
+
const error = new LseHttpError(response.status, await readErrorDetail(response));
|
|
69
|
+
throwIfLseAuthError(error);
|
|
70
|
+
lastError = error;
|
|
71
|
+
if (response.status === 429 && error.detail.toLowerCase().includes("allowance")) {
|
|
72
|
+
markExhausted();
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const retryable = response.status === 429 || response.status >= 500;
|
|
76
|
+
if (!retryable || attempt === MAX_RETRIES)
|
|
77
|
+
throw error;
|
|
78
|
+
const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"));
|
|
79
|
+
await sleep(retryAfterMs === undefined
|
|
80
|
+
? RETRY_DELAY_MS * (attempt + 1)
|
|
81
|
+
: Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
|
|
82
|
+
}
|
|
83
|
+
throw lastError ?? new Error("LSE request failed without an error");
|
|
84
|
+
}
|
|
85
|
+
export async function getLseCandles(symbol, timeframe, opts = {}) {
|
|
86
|
+
const params = { symbol, timeframe };
|
|
87
|
+
if (opts.start !== undefined)
|
|
88
|
+
params.start = opts.start.slice(0, 10);
|
|
89
|
+
if (opts.end !== undefined)
|
|
90
|
+
params.end = opts.end;
|
|
91
|
+
if (opts.order !== undefined)
|
|
92
|
+
params.order = opts.order;
|
|
93
|
+
if (opts.limit !== undefined)
|
|
94
|
+
params.limit = String(opts.limit);
|
|
95
|
+
const cacheKey = `lse:candles:${new URLSearchParams(params)}`;
|
|
96
|
+
const cached = cache.get(cacheKey);
|
|
97
|
+
if (cached)
|
|
98
|
+
return cached;
|
|
99
|
+
try {
|
|
100
|
+
await rateLimiter.acquire("lse");
|
|
101
|
+
const rows = await lseGet("/candles", params);
|
|
102
|
+
if (!isValidLseCandlePayload(rows)) {
|
|
103
|
+
throw new Error("LSE candles response did not contain valid candle rows");
|
|
104
|
+
}
|
|
105
|
+
cache.set(cacheKey, rows, TTL.CANDLES);
|
|
106
|
+
return rows;
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
throwIfLseAuthError(error);
|
|
110
|
+
const stale = cache.getStale(cacheKey, STALE_LIMIT.CANDLES);
|
|
111
|
+
if (stale)
|
|
112
|
+
return stale.value;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function isValidLseCandlePayload(value) {
|
|
117
|
+
return (Array.isArray(value) &&
|
|
118
|
+
value.length > 0 &&
|
|
119
|
+
value.every((row) => {
|
|
120
|
+
if (typeof row !== "object" || row === null)
|
|
121
|
+
return false;
|
|
122
|
+
const candle = row;
|
|
123
|
+
const timestamp = typeof candle.ts === "string"
|
|
124
|
+
? Date.parse(/(?:Z|[+-]\d{2}:\d{2})$/i.test(candle.ts) ? candle.ts : `${candle.ts}Z`)
|
|
125
|
+
: Number.NaN;
|
|
126
|
+
const prices = [candle.open, candle.high, candle.low, candle.close];
|
|
127
|
+
return (Number.isFinite(timestamp) &&
|
|
128
|
+
prices.every((price) => typeof price === "number" && Number.isFinite(price) && price > 0) &&
|
|
129
|
+
(candle.high ?? 0) >= Math.max(candle.open ?? 0, candle.close ?? 0) &&
|
|
130
|
+
(candle.low ?? Number.POSITIVE_INFINITY) <=
|
|
131
|
+
Math.min(candle.open ?? Number.POSITIVE_INFINITY, candle.close ?? Number.POSITIVE_INFINITY) &&
|
|
132
|
+
typeof candle.volume === "number" &&
|
|
133
|
+
Number.isFinite(candle.volume) &&
|
|
134
|
+
candle.volume >= 0);
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
export async function getLseFinancialReports(symbol, reportType, opts = {}) {
|
|
138
|
+
const params = { symbol, report_type: reportType };
|
|
139
|
+
if (opts.period !== undefined)
|
|
140
|
+
params.period = opts.period;
|
|
141
|
+
if (opts.start !== undefined)
|
|
142
|
+
params.start = opts.start;
|
|
143
|
+
if (opts.end !== undefined)
|
|
144
|
+
params.end = opts.end;
|
|
145
|
+
if (opts.order !== undefined)
|
|
146
|
+
params.order = opts.order;
|
|
147
|
+
if (opts.limit !== undefined)
|
|
148
|
+
params.limit = String(opts.limit);
|
|
149
|
+
const cacheKey = `lse:financial_reports:${new URLSearchParams(params)}`;
|
|
150
|
+
const cached = cache.get(cacheKey);
|
|
151
|
+
if (cached)
|
|
152
|
+
return cached;
|
|
153
|
+
try {
|
|
154
|
+
await rateLimiter.acquire("lse");
|
|
155
|
+
const rows = await lseGet("/ref/financial_reports", params);
|
|
156
|
+
cache.set(cacheKey, rows, TTL.FINANCIAL_REPORTS);
|
|
157
|
+
return rows;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
throwIfLseAuthError(error);
|
|
161
|
+
const stale = cache.getStale(cacheKey, STALE_LIMIT.FINANCIAL_REPORTS);
|
|
162
|
+
if (stale)
|
|
163
|
+
return stale.value;
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function parseNum(value) {
|
|
168
|
+
if (typeof value !== "number" && typeof value !== "string")
|
|
169
|
+
return undefined;
|
|
170
|
+
if (typeof value === "string" && value.trim() === "")
|
|
171
|
+
return undefined;
|
|
172
|
+
const parsed = Number(value);
|
|
173
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
174
|
+
}
|
|
175
|
+
function parseReportData(row) {
|
|
176
|
+
if (typeof row.data !== "string")
|
|
177
|
+
return undefined;
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(row.data);
|
|
180
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
181
|
+
? parsed
|
|
182
|
+
: undefined;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function indexReportData(rows) {
|
|
189
|
+
const indexed = new Map();
|
|
190
|
+
for (const row of rows) {
|
|
191
|
+
if (typeof row.date !== "string")
|
|
192
|
+
continue;
|
|
193
|
+
const data = parseReportData(row);
|
|
194
|
+
if (data)
|
|
195
|
+
indexed.set(row.date, data);
|
|
196
|
+
}
|
|
197
|
+
return indexed;
|
|
198
|
+
}
|
|
199
|
+
export async function getLseFinancials(symbol) {
|
|
200
|
+
const [incomeRows, balanceRows, cashflowRows] = await Promise.all([
|
|
201
|
+
getLseFinancialReports(symbol, "income", { period: "FY" }),
|
|
202
|
+
getLseFinancialReports(symbol, "balance", { period: "FY" }),
|
|
203
|
+
getLseFinancialReports(symbol, "cashflow", { period: "FY" }),
|
|
204
|
+
]);
|
|
205
|
+
const balanceByDate = indexReportData(balanceRows);
|
|
206
|
+
const cashflowByDate = indexReportData(cashflowRows);
|
|
207
|
+
const statements = incomeRows
|
|
208
|
+
.flatMap((row) => {
|
|
209
|
+
if (typeof row.date !== "string")
|
|
210
|
+
return [];
|
|
211
|
+
const income = parseReportData(row);
|
|
212
|
+
return income ? [{ date: row.date, income }] : [];
|
|
213
|
+
})
|
|
214
|
+
.sort((a, b) => b.date.localeCompare(a.date))
|
|
215
|
+
.flatMap(({ date, income }) => {
|
|
216
|
+
const balance = balanceByDate.get(date);
|
|
217
|
+
const cashflow = cashflowByDate.get(date);
|
|
218
|
+
if (!balance || !cashflow)
|
|
219
|
+
return [];
|
|
220
|
+
const revenue = parseNum(income.revenue);
|
|
221
|
+
const grossProfit = parseNum(income.grossProfit);
|
|
222
|
+
const operatingIncome = parseNum(income.operatingIncome);
|
|
223
|
+
const netIncome = parseNum(income.netIncome);
|
|
224
|
+
const eps = parseNum(income.eps);
|
|
225
|
+
const totalAssets = parseNum(balance.totalAssets);
|
|
226
|
+
const totalLiabilities = parseNum(balance.totalLiabilities);
|
|
227
|
+
const totalEquity = parseNum(balance.totalEquity);
|
|
228
|
+
const operatingCashFlow = parseNum(cashflow.operatingCashFlow);
|
|
229
|
+
const reportedFreeCashFlow = parseNum(cashflow.freeCashFlow);
|
|
230
|
+
const capitalExpenditure = parseNum(cashflow.capitalExpenditure);
|
|
231
|
+
const freeCashFlow = reportedFreeCashFlow ??
|
|
232
|
+
(operatingCashFlow !== undefined && capitalExpenditure !== undefined
|
|
233
|
+
? // LSE capex is negative; subtract its magnitude to match AV's positive-capex semantics.
|
|
234
|
+
operatingCashFlow - Math.abs(capitalExpenditure)
|
|
235
|
+
: undefined);
|
|
236
|
+
if (revenue === undefined ||
|
|
237
|
+
grossProfit === undefined ||
|
|
238
|
+
operatingIncome === undefined ||
|
|
239
|
+
netIncome === undefined ||
|
|
240
|
+
eps === undefined ||
|
|
241
|
+
totalAssets === undefined ||
|
|
242
|
+
totalLiabilities === undefined ||
|
|
243
|
+
totalEquity === undefined ||
|
|
244
|
+
operatingCashFlow === undefined ||
|
|
245
|
+
freeCashFlow === undefined) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
return [
|
|
249
|
+
{
|
|
250
|
+
fiscalDate: date,
|
|
251
|
+
revenue,
|
|
252
|
+
grossProfit,
|
|
253
|
+
operatingIncome,
|
|
254
|
+
netIncome,
|
|
255
|
+
eps,
|
|
256
|
+
totalAssets,
|
|
257
|
+
totalLiabilities,
|
|
258
|
+
totalEquity,
|
|
259
|
+
operatingCashFlow,
|
|
260
|
+
freeCashFlow,
|
|
261
|
+
totalDebt: parseNum(balance.totalDebt),
|
|
262
|
+
cashAndEquivalents: parseNum(balance.cashAndCashEquivalents),
|
|
263
|
+
sharesOutstanding: parseNum(income.weightedAverageShsOut),
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
})
|
|
267
|
+
.slice(0, 4);
|
|
268
|
+
if (statements.length === 0) {
|
|
269
|
+
throw new Error(`LSE returned no complete financial statements for ${symbol}`);
|
|
270
|
+
}
|
|
271
|
+
return statements;
|
|
272
|
+
}
|
|
273
|
+
export function toLseTimeframe(interval) {
|
|
274
|
+
const timeframes = {
|
|
275
|
+
"1m": "1m",
|
|
276
|
+
"5m": "5m",
|
|
277
|
+
"15m": "15m",
|
|
278
|
+
"1h": "1h",
|
|
279
|
+
"1d": "1d",
|
|
280
|
+
"1wk": "1w",
|
|
281
|
+
"1mo": "1mo",
|
|
282
|
+
};
|
|
283
|
+
return timeframes[interval];
|
|
284
|
+
}
|
|
@@ -51,7 +51,13 @@ function marketToQuotes(market, event) {
|
|
|
51
51
|
const liquidityUsd = numberValue(market.liquidityNum) ??
|
|
52
52
|
numberValue(market.liquidity) ??
|
|
53
53
|
numberValue(event?.liquidity);
|
|
54
|
-
const
|
|
54
|
+
const rawCloseDate = stringValue(market.endDate) ?? normalizeDateOnly(market.endDateIso ?? event?.endDate);
|
|
55
|
+
// A past close date on a market Gamma explicitly marks active is the same
|
|
56
|
+
// stale metadata isOpenMarket ignores; presenting it as a close date would
|
|
57
|
+
// tell users an open market already closed, so it is omitted instead.
|
|
58
|
+
const closeDate = rawCloseDate !== undefined && booleanValue(market.active) === true && isPastDate(rawCloseDate)
|
|
59
|
+
? undefined
|
|
60
|
+
: rawCloseDate;
|
|
55
61
|
const url = marketUrl(market, event);
|
|
56
62
|
const asOf = stringValue(market.updatedAt) ?? stringValue(event?.updatedAt);
|
|
57
63
|
return outcomes.flatMap((outcome, index) => {
|
|
@@ -76,15 +82,26 @@ function marketToQuotes(market, event) {
|
|
|
76
82
|
});
|
|
77
83
|
}
|
|
78
84
|
function isOpenMarket(market) {
|
|
79
|
-
|
|
85
|
+
const closed = booleanValue(market.closed);
|
|
86
|
+
const active = booleanValue(market.active);
|
|
87
|
+
if (closed === true)
|
|
80
88
|
return false;
|
|
81
|
-
if (
|
|
89
|
+
if (active === false)
|
|
82
90
|
return false;
|
|
91
|
+
// An explicit affirmative active flag is Gamma's authoritative openness
|
|
92
|
+
// signal and beats the endDate heuristic, whose metadata is demonstrably
|
|
93
|
+
// stale on live markets. A lone `closed: false` only says "not settled",
|
|
94
|
+
// so it still falls through to the date check.
|
|
95
|
+
if (active === true)
|
|
96
|
+
return true;
|
|
83
97
|
const endDate = stringValue(market.endDate) ?? stringValue(market.endDateIso);
|
|
84
98
|
if (!endDate)
|
|
85
99
|
return true;
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
return !isPastDate(endDate);
|
|
101
|
+
}
|
|
102
|
+
function isPastDate(value) {
|
|
103
|
+
const parsed = new Date(value);
|
|
104
|
+
return !Number.isNaN(parsed.getTime()) && parsed.getTime() <= Date.now();
|
|
88
105
|
}
|
|
89
106
|
function normalizeLimit(limit) {
|
|
90
107
|
if (!Number.isFinite(limit))
|
|
@@ -310,6 +310,7 @@ export async function getHistory(symbol, range = "6mo", interval = "1d") {
|
|
|
310
310
|
const ohlcv = timestamps
|
|
311
311
|
.map((ts, i) => ({
|
|
312
312
|
date: new Date(ts * 1000).toISOString().split("T")[0],
|
|
313
|
+
timestamp: ts,
|
|
313
314
|
open: quotes.open[i],
|
|
314
315
|
high: quotes.high[i],
|
|
315
316
|
low: quotes.low[i],
|
package/dist/routing/router.js
CHANGED
|
@@ -638,6 +638,13 @@ export function postProcessRouterOutput(text, output, inputContext) {
|
|
|
638
638
|
const selectedToolBundles = isConceptualEducationRequest(text, next)
|
|
639
639
|
? []
|
|
640
640
|
: selectToolBundles(next);
|
|
641
|
+
if (isExplicitMacroDataRequest(text) && !selectedToolBundles.includes("macro")) {
|
|
642
|
+
selectedToolBundles.push("macro");
|
|
643
|
+
diagnostics.push({
|
|
644
|
+
code: "macro_tool_bundle_recovered",
|
|
645
|
+
message: "explicit macro data request retained the macro tool bundle",
|
|
646
|
+
});
|
|
647
|
+
}
|
|
641
648
|
if (selectedToolBundles.length === 0 && isConceptualEducationRequest(text, next)) {
|
|
642
649
|
diagnostics.push({
|
|
643
650
|
code: "conceptual_education_no_tools",
|
|
@@ -659,7 +666,7 @@ export function postProcessRouterOutput(text, output, inputContext) {
|
|
|
659
666
|
});
|
|
660
667
|
}
|
|
661
668
|
function isExplicitMacroDataRequest(text) {
|
|
662
|
-
return /\b(?:get_economic_data|fred|cpi|inflation|fed\s+funds?|unemployment|gdp|macro)\b/i.test(text);
|
|
669
|
+
return /\b(?:get_economic_data|fred|cpi|inflation|fed\s+funds?|unemployment|gdp|macro|fear\s*(?:&|and)\s*greed)\b/i.test(text);
|
|
663
670
|
}
|
|
664
671
|
function isConceptualEducationRequest(text, output) {
|
|
665
672
|
if (output.routeKind !== "agent_task")
|
package/dist/runtime/evidence.js
CHANGED
|
@@ -11,7 +11,7 @@ export function toEvidenceRecord(label, result, providerId) {
|
|
|
11
11
|
provenance: {
|
|
12
12
|
source: result.stale ? "stale_cache" : "fetched",
|
|
13
13
|
timestamp: result.timestamp,
|
|
14
|
-
provider: providerId,
|
|
14
|
+
provider: providerId ?? result.provider,
|
|
15
15
|
confidence: result.stale ? 0.5 : undefined,
|
|
16
16
|
},
|
|
17
17
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ModelRuntime, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { MemoryStorage } from "../memory/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Alias for the session-manager handle extensions receive via
|
|
@@ -52,7 +52,7 @@ export declare class SessionCoordinator {
|
|
|
52
52
|
/** Run setup flow. */
|
|
53
53
|
runSetup(pi: ExtensionAPI, ctx: ExtensionContext | ExtensionCommandContext, options: {
|
|
54
54
|
mode: "startup" | "manual";
|
|
55
|
-
}): Promise<"ready" | "shutdown" | "cancelled">;
|
|
55
|
+
}, modelRuntime?: ModelRuntime): Promise<"ready" | "shutdown" | "cancelled">;
|
|
56
56
|
/** Extract and persist user preferences from natural language. */
|
|
57
57
|
extractAndStorePreferences(text: string): void;
|
|
58
58
|
/** Record a workflow run in storage. */
|
|
@@ -112,8 +112,8 @@ export class SessionCoordinator {
|
|
|
112
112
|
this.sessionId = sessionId;
|
|
113
113
|
}
|
|
114
114
|
/** Run setup flow. */
|
|
115
|
-
async runSetup(pi, ctx, options) {
|
|
116
|
-
return runOpenCandleSetup(pi, ctx, options);
|
|
115
|
+
async runSetup(pi, ctx, options, modelRuntime) {
|
|
116
|
+
return runOpenCandleSetup(pi, ctx, options, modelRuntime);
|
|
117
117
|
}
|
|
118
118
|
/** Extract and persist user preferences from natural language. */
|
|
119
119
|
extractAndStorePreferences(text) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getConfig } from "../../config.js";
|
|
3
|
+
import { isOverSoftThreshold } from "../../infra/lse-byte-budget.js";
|
|
3
4
|
import { getFinancials, getOverview } from "../../providers/alpha-vantage.js";
|
|
5
|
+
import { getLseFinancials } from "../../providers/lse.js";
|
|
4
6
|
import { ProviderCredentialError } from "../../providers/provider-credential-error.js";
|
|
5
7
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
8
|
import { getQuote, getYahooFinancials } from "../../providers/yahoo-finance.js";
|
|
@@ -129,10 +131,33 @@ export const dcfTool = {
|
|
|
129
131
|
const symbol = args.symbol.toUpperCase();
|
|
130
132
|
const config = getConfig();
|
|
131
133
|
const alphaVantageApiKey = config.alphaVantageApiKey;
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
const quotePromise = wrapProvider("yahoo", () => getQuote(symbol));
|
|
135
|
+
const lseEligible = !!config.lseApiKey && !isOverSoftThreshold();
|
|
136
|
+
let lseFinancialsResult;
|
|
137
|
+
if (lseEligible) {
|
|
138
|
+
try {
|
|
139
|
+
lseFinancialsResult = await wrapProvider("lse", () => getLseFinancials(symbol));
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof ProviderCredentialError) {
|
|
143
|
+
lseFinancialsResult = {
|
|
144
|
+
status: "unavailable",
|
|
145
|
+
reason: `London Strategic Edge credential ${error.reason}`,
|
|
146
|
+
provider: "lse",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const lseIsFresh = lseFinancialsResult?.status === "ok" &&
|
|
155
|
+
lseFinancialsResult.data.length > 0 &&
|
|
156
|
+
!lseFinancialsResult.stale;
|
|
157
|
+
const alphaVantageFinancialsResult = lseIsFresh
|
|
158
|
+
? undefined
|
|
159
|
+
: await optionalAlphaVantage(alphaVantageApiKey, (apiKey) => getFinancials(symbol, apiKey));
|
|
160
|
+
const quoteResult = await quotePromise;
|
|
136
161
|
const missing = [];
|
|
137
162
|
if (quoteResult.status === "unavailable")
|
|
138
163
|
missing.push(`stock quote (${quoteResult.reason})`);
|
|
@@ -159,17 +184,21 @@ export const dcfTool = {
|
|
|
159
184
|
details: null,
|
|
160
185
|
};
|
|
161
186
|
}
|
|
162
|
-
let financialsSource = "Alpha Vantage";
|
|
187
|
+
let financialsSource = lseIsFresh ? "London Strategic Edge" : "Alpha Vantage";
|
|
163
188
|
let financials;
|
|
164
|
-
if (
|
|
189
|
+
if (lseIsFresh && lseFinancialsResult?.status === "ok") {
|
|
190
|
+
financials = lseFinancialsResult.data;
|
|
191
|
+
}
|
|
192
|
+
else if (alphaVantageFinancialsResult?.status === "ok" &&
|
|
193
|
+
!alphaVantageFinancialsResult.stale) {
|
|
165
194
|
financials = alphaVantageFinancialsResult.data;
|
|
166
195
|
}
|
|
167
196
|
else {
|
|
168
|
-
const alphaVantageReason = alphaVantageFinancialsResult
|
|
197
|
+
const alphaVantageReason = alphaVantageFinancialsResult?.status === "ok"
|
|
169
198
|
? `stale cached financial statements${alphaVantageFinancialsResult.timestamp
|
|
170
199
|
? ` from ${alphaVantageFinancialsResult.timestamp}`
|
|
171
200
|
: ""}`
|
|
172
|
-
: alphaVantageFinancialsResult
|
|
201
|
+
: (alphaVantageFinancialsResult?.reason ?? "Alpha Vantage unavailable");
|
|
173
202
|
const yahooFinancialsResult = await wrapProvider("yahoo", () => getYahooFinancials(symbol));
|
|
174
203
|
if (yahooFinancialsResult.status === "unavailable") {
|
|
175
204
|
return {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getConfig } from "../../config.js";
|
|
3
|
+
import { isOverSoftThreshold } from "../../infra/lse-byte-budget.js";
|
|
3
4
|
import { withCredentialCheck } from "../../onboarding/tool-helpers.js";
|
|
4
5
|
import { getFinancials } from "../../providers/alpha-vantage.js";
|
|
6
|
+
import { getLseFinancials } from "../../providers/lse.js";
|
|
7
|
+
import { ProviderCredentialError } from "../../providers/provider-credential-error.js";
|
|
5
8
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
9
|
const params = Type.Object({
|
|
7
10
|
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT)" }),
|
|
@@ -9,20 +12,18 @@ const params = Type.Object({
|
|
|
9
12
|
export const financialsTool = {
|
|
10
13
|
name: "get_financials",
|
|
11
14
|
label: "Financial Statements",
|
|
12
|
-
description: "Get annual income statement data: revenue, gross profit, operating income, net income, and EPS.
|
|
15
|
+
description: "Get annual income statement data: revenue, gross profit, operating income, net income, and EPS. Uses London Strategic Edge or Alpha Vantage.",
|
|
13
16
|
parameters: params,
|
|
14
17
|
async execute(_toolCallId, args) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
throw new Error("Alpha Vantage credential is missing.");
|
|
19
|
-
const result = await wrapProvider("alphavantage", () => getFinancials(args.symbol.toUpperCase(), apiKey));
|
|
18
|
+
const symbol = args.symbol.toUpperCase();
|
|
19
|
+
const config = getConfig();
|
|
20
|
+
const renderResult = (result, source) => {
|
|
20
21
|
if (result.status === "unavailable") {
|
|
21
22
|
return {
|
|
22
23
|
content: [
|
|
23
24
|
{
|
|
24
25
|
type: "text",
|
|
25
|
-
text: `⚠ Financial statements unavailable for ${
|
|
26
|
+
text: `⚠ Financial statements unavailable for ${symbol} (${result.reason}). Analysis will proceed without financials.`,
|
|
26
27
|
},
|
|
27
28
|
],
|
|
28
29
|
details: [],
|
|
@@ -35,10 +36,87 @@ export const financialsTool = {
|
|
|
35
36
|
details: [],
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
|
-
const header = `${
|
|
39
|
+
const header = `${symbol} — Annual Income Statement (${statements.length} years)`;
|
|
39
40
|
const rows = statements.map((s) => `${s.fiscalDate} | Rev: $${fmt(s.revenue)} | GP: $${fmt(s.grossProfit)} | OpInc: $${fmt(s.operatingIncome)} | Net: $${fmt(s.netIncome)}`);
|
|
40
|
-
const
|
|
41
|
-
|
|
41
|
+
const sourceLine = source ? [`Source: ${source}`] : [];
|
|
42
|
+
const staleLine = result.stale
|
|
43
|
+
? [`Data status: stale cache from ${result.timestamp} (provider unavailable)`]
|
|
44
|
+
: [];
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: [header, ...sourceLine, ...staleLine, ...rows].join("\n"),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
details: statements,
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const runAlphaVantageOnly = () => withCredentialCheck("alpha_vantage", async () => {
|
|
56
|
+
const apiKey = getConfig().alphaVantageApiKey;
|
|
57
|
+
if (!apiKey)
|
|
58
|
+
throw new Error("Alpha Vantage credential is missing.");
|
|
59
|
+
const result = await wrapProvider("alphavantage", () => getFinancials(symbol, apiKey));
|
|
60
|
+
return renderResult(result);
|
|
61
|
+
});
|
|
62
|
+
const lseEligible = !!config.lseApiKey && !isOverSoftThreshold();
|
|
63
|
+
if (!lseEligible)
|
|
64
|
+
return runAlphaVantageOnly();
|
|
65
|
+
const lseEntry = {
|
|
66
|
+
provider: "lse",
|
|
67
|
+
fn: async () => {
|
|
68
|
+
try {
|
|
69
|
+
const statements = await getLseFinancials(symbol);
|
|
70
|
+
if (statements.length === 0)
|
|
71
|
+
throw new Error("no financial statements returned");
|
|
72
|
+
return statements;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error instanceof ProviderCredentialError) {
|
|
76
|
+
throw new Error(`London Strategic Edge credential ${error.reason}`);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
if (!config.alphaVantageApiKey) {
|
|
83
|
+
const result = await wrapProvider(lseEntry.provider, lseEntry.fn);
|
|
84
|
+
if (result.status === "ok")
|
|
85
|
+
return renderResult(result, "London Strategic Edge");
|
|
86
|
+
return runAlphaVantageOnly();
|
|
87
|
+
}
|
|
88
|
+
return withCredentialCheck("alpha_vantage", async () => {
|
|
89
|
+
const apiKey = config.alphaVantageApiKey;
|
|
90
|
+
if (!apiKey)
|
|
91
|
+
throw new Error("Alpha Vantage credential is missing.");
|
|
92
|
+
const lseResult = await wrapProvider(lseEntry.provider, lseEntry.fn);
|
|
93
|
+
if (lseResult.status === "ok" && !lseResult.stale) {
|
|
94
|
+
return renderResult(lseResult, "London Strategic Edge");
|
|
95
|
+
}
|
|
96
|
+
let alphaVantageResult;
|
|
97
|
+
try {
|
|
98
|
+
alphaVantageResult = await wrapProvider("alphavantage", () => getFinancials(symbol, apiKey));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof ProviderCredentialError && lseResult.status === "ok") {
|
|
102
|
+
return renderResult(lseResult, "London Strategic Edge");
|
|
103
|
+
}
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
if (alphaVantageResult.status === "ok" && !alphaVantageResult.stale) {
|
|
107
|
+
return renderResult(alphaVantageResult, "Alpha Vantage");
|
|
108
|
+
}
|
|
109
|
+
if (lseResult.status === "ok") {
|
|
110
|
+
return renderResult(lseResult, "London Strategic Edge");
|
|
111
|
+
}
|
|
112
|
+
if (alphaVantageResult.status === "ok") {
|
|
113
|
+
return renderResult(alphaVantageResult, "Alpha Vantage");
|
|
114
|
+
}
|
|
115
|
+
return renderResult({
|
|
116
|
+
status: "unavailable",
|
|
117
|
+
provider: "fallback",
|
|
118
|
+
reason: `lse: ${lseResult.reason}; alphavantage: ${alphaVantageResult.reason}`,
|
|
119
|
+
});
|
|
42
120
|
});
|
|
43
121
|
},
|
|
44
122
|
};
|