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
|
@@ -2,8 +2,135 @@ import { buildFreshnessStamp } from "../../infra/freshness.js";
|
|
|
2
2
|
import { isZeroFilledQuote, searchYahooInstruments } from "../../market-state/resolve.js";
|
|
3
3
|
import { MarketStateService } from "../../market-state/service.js";
|
|
4
4
|
import { initDefaultDatabase } from "../../memory/sqlite.js";
|
|
5
|
+
import { getProvider } from "../../onboarding/providers.js";
|
|
5
6
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
|
-
import { getQuote } from "../../providers/yahoo-finance.js";
|
|
7
|
+
import { getHistory, getQuote, getYahooCompanyOverview, } from "../../providers/yahoo-finance.js";
|
|
8
|
+
import { fetchHistoryWithFallback, HISTORY_INTERVALS, } from "../../tools/market/stock-history.js";
|
|
9
|
+
import { HistorySnapshotStore } from "./history-snapshot-store.js";
|
|
10
|
+
const instrumentHistorySnapshotStore = new HistorySnapshotStore();
|
|
11
|
+
const DEFAULT_HISTORY_RANGE_MAP = {
|
|
12
|
+
"1D": { range: "1d", interval: "5m" },
|
|
13
|
+
"5D": { range: "5d", interval: "15m" },
|
|
14
|
+
"1M": { range: "1mo", interval: "1h" },
|
|
15
|
+
"6M": { range: "6mo", interval: "1d" },
|
|
16
|
+
YTD: { range: "ytd", interval: "1d" },
|
|
17
|
+
"1Y": { range: "1y", interval: "1d" },
|
|
18
|
+
"5Y": { range: "5y", interval: "1wk" },
|
|
19
|
+
MAX: { range: "max", interval: "1mo" },
|
|
20
|
+
};
|
|
21
|
+
const HISTORY_RANGE_SPAN_DAYS = {
|
|
22
|
+
"1D": 1,
|
|
23
|
+
"5D": 5,
|
|
24
|
+
"1M": 31,
|
|
25
|
+
"6M": 183,
|
|
26
|
+
YTD: 366,
|
|
27
|
+
"1Y": 366,
|
|
28
|
+
"5Y": 1_827,
|
|
29
|
+
MAX: Number.POSITIVE_INFINITY,
|
|
30
|
+
};
|
|
31
|
+
const INTRADAY_DEPTH_CAP_DAYS = {
|
|
32
|
+
"1m": 7,
|
|
33
|
+
"5m": 60,
|
|
34
|
+
"15m": 60,
|
|
35
|
+
"1h": 730,
|
|
36
|
+
};
|
|
37
|
+
const DAILY_OR_LONGER_INTERVALS = new Set(["1d", "1wk", "1mo"]);
|
|
38
|
+
const HISTORY_PROVIDER_REGISTRY_IDS = {
|
|
39
|
+
yahoo: "yahoo",
|
|
40
|
+
alphavantage: "alpha_vantage",
|
|
41
|
+
lse: "lse",
|
|
42
|
+
};
|
|
43
|
+
function historyProviderDisplayName(provider) {
|
|
44
|
+
if (!(provider && provider in HISTORY_PROVIDER_REGISTRY_IDS))
|
|
45
|
+
return null;
|
|
46
|
+
const providerId = HISTORY_PROVIDER_REGISTRY_IDS[provider];
|
|
47
|
+
return getProvider(providerId).displayName;
|
|
48
|
+
}
|
|
49
|
+
export function resolveHistoryRange(rangeLabel, intervalOverride) {
|
|
50
|
+
const resolved = DEFAULT_HISTORY_RANGE_MAP[rangeLabel];
|
|
51
|
+
if (!resolved) {
|
|
52
|
+
return { status: "invalid_request", reason: `Unknown history range: ${rangeLabel}` };
|
|
53
|
+
}
|
|
54
|
+
if (intervalOverride === undefined)
|
|
55
|
+
return resolved;
|
|
56
|
+
if (!isHistoryInterval(intervalOverride)) {
|
|
57
|
+
return { status: "invalid_request", reason: `Unknown history interval: ${intervalOverride}` };
|
|
58
|
+
}
|
|
59
|
+
const depthCapDays = INTRADAY_DEPTH_CAP_DAYS[intervalOverride];
|
|
60
|
+
if (depthCapDays !== undefined &&
|
|
61
|
+
HISTORY_RANGE_SPAN_DAYS[rangeLabel] > depthCapDays) {
|
|
62
|
+
return {
|
|
63
|
+
status: "invalid_request",
|
|
64
|
+
reason: `History range ${rangeLabel} exceeds the depth cap for interval ${intervalOverride}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { range: resolved.range, interval: intervalOverride };
|
|
68
|
+
}
|
|
69
|
+
function isHistoryInterval(value) {
|
|
70
|
+
return HISTORY_INTERVALS.includes(value);
|
|
71
|
+
}
|
|
72
|
+
export async function getInstrumentHistorySnapshot(symbol, rangeLabel = "1D", intervalOverride) {
|
|
73
|
+
const resolved = resolveHistoryRange(rangeLabel, intervalOverride);
|
|
74
|
+
if ("status" in resolved)
|
|
75
|
+
return resolved;
|
|
76
|
+
const key = `${symbol}|${resolved.range}|${resolved.interval}`;
|
|
77
|
+
return instrumentHistorySnapshotStore.get(key, async () => {
|
|
78
|
+
const result = await fetchHistoryWithFallback(symbol, resolved.range, resolved.interval);
|
|
79
|
+
if (result.status === "unavailable") {
|
|
80
|
+
return { status: "unavailable", reason: result.reason };
|
|
81
|
+
}
|
|
82
|
+
const source = historyProviderDisplayName(result.provider);
|
|
83
|
+
if (source === null) {
|
|
84
|
+
return { status: "unavailable", reason: "History provider attribution is unavailable" };
|
|
85
|
+
}
|
|
86
|
+
const bars = result.data.filter((bar) => Number.isFinite(bar.close));
|
|
87
|
+
const dataAsOf = bars.at(-1)?.date;
|
|
88
|
+
const chartBars = [];
|
|
89
|
+
for (const bar of bars) {
|
|
90
|
+
let time = bar.timestamp;
|
|
91
|
+
if (typeof time !== "number" || !Number.isFinite(time)) {
|
|
92
|
+
if (DAILY_OR_LONGER_INTERVALS.has(resolved.interval)) {
|
|
93
|
+
time = Date.parse(`${bar.date}T00:00:00Z`) / 1_000;
|
|
94
|
+
}
|
|
95
|
+
if (typeof time !== "number" || !Number.isFinite(time)) {
|
|
96
|
+
return {
|
|
97
|
+
status: "unavailable",
|
|
98
|
+
reason: `Historical bars for ${resolved.interval} are missing finite timestamps`,
|
|
99
|
+
dataAsOf,
|
|
100
|
+
stale: result.stale === true || undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
chartBars.push({
|
|
105
|
+
time,
|
|
106
|
+
open: bar.open,
|
|
107
|
+
high: bar.high,
|
|
108
|
+
low: bar.low,
|
|
109
|
+
close: bar.close,
|
|
110
|
+
volume: bar.volume,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// Design D3: daily+ bars can honestly derive the previous close from the prior bar.
|
|
114
|
+
const prevClose = DAILY_OR_LONGER_INTERVALS.has(resolved.interval) && bars.length >= 2
|
|
115
|
+
? bars[bars.length - 2].close
|
|
116
|
+
: null;
|
|
117
|
+
return {
|
|
118
|
+
status: "ok",
|
|
119
|
+
symbol,
|
|
120
|
+
range: rangeLabel,
|
|
121
|
+
interval: resolved.interval,
|
|
122
|
+
source,
|
|
123
|
+
fetchedAt: result.timestamp,
|
|
124
|
+
dataAsOf,
|
|
125
|
+
stale: result.stale === true,
|
|
126
|
+
prevClose,
|
|
127
|
+
bars: chartBars,
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function resetInstrumentHistoryMemoForTests() {
|
|
132
|
+
instrumentHistorySnapshotStore.clear();
|
|
133
|
+
}
|
|
7
134
|
export function createSavedSymbolsMemo(load, options = {}) {
|
|
8
135
|
const ttlMs = options.ttlMs ?? 30_000;
|
|
9
136
|
const now = options.now ?? Date.now;
|
|
@@ -96,8 +223,13 @@ export async function buildMarketStateQuoteSnapshot(db) {
|
|
|
96
223
|
]),
|
|
97
224
|
];
|
|
98
225
|
const quoteMap = new Map();
|
|
226
|
+
const sparklineMap = new Map();
|
|
99
227
|
for (const symbol of symbols) {
|
|
100
|
-
|
|
228
|
+
const quote = await fetchQuoteSnapshot(symbol);
|
|
229
|
+
quoteMap.set(symbol, quote);
|
|
230
|
+
sparklineMap.set(symbol, quote.status === "ok"
|
|
231
|
+
? await fetchSparklineSnapshot(symbol)
|
|
232
|
+
: unavailableSparkline(quote.reason));
|
|
101
233
|
}
|
|
102
234
|
const generatedAt = new Date().toISOString();
|
|
103
235
|
const watchlistQuotes = watchlist.map((item) => {
|
|
@@ -109,6 +241,7 @@ export async function buildMarketStateQuoteSnapshot(db) {
|
|
|
109
241
|
symbol: item.symbol,
|
|
110
242
|
status: "unavailable",
|
|
111
243
|
reason: quote?.reason ?? "quote unavailable",
|
|
244
|
+
sparkline: sparklineMap.get(item.symbol) ?? unavailableSparkline("History unavailable"),
|
|
112
245
|
};
|
|
113
246
|
}
|
|
114
247
|
return {
|
|
@@ -134,12 +267,14 @@ export async function buildMarketStateQuoteSnapshot(db) {
|
|
|
134
267
|
extendedChangePercent: quote.extendedChangePercent,
|
|
135
268
|
extendedAsOf: quote.extendedAsOf,
|
|
136
269
|
stale: quote.stale,
|
|
270
|
+
sparkline: sparklineMap.get(item.symbol) ?? unavailableSparkline("History unavailable"),
|
|
137
271
|
};
|
|
138
272
|
});
|
|
139
273
|
const portfolioResults = portfolios.map((portfolio) => buildPortfolioQuoteResult({
|
|
140
274
|
portfolio,
|
|
141
275
|
lots: portfolioLots.filter((lot) => lot.portfolioId === portfolio.id),
|
|
142
276
|
quoteMap,
|
|
277
|
+
sparklineMap,
|
|
143
278
|
}));
|
|
144
279
|
const defaultPortfolio = portfolios.find((portfolio) => portfolio.isDefault) ?? portfolios[0];
|
|
145
280
|
const defaultResult = portfolioResults.find((result) => result.summary.portfolioId === defaultPortfolio.id) ??
|
|
@@ -157,7 +292,7 @@ export async function buildMarketStateQuoteSnapshot(db) {
|
|
|
157
292
|
ownedDb.close();
|
|
158
293
|
}
|
|
159
294
|
}
|
|
160
|
-
function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
|
|
295
|
+
function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, sparklineMap, }) {
|
|
161
296
|
const baseCurrency = portfolio.baseCurrency ?? "USD";
|
|
162
297
|
const portfolioQuotes = lots.map((lot) => {
|
|
163
298
|
const quote = quoteMap.get(lot.symbol);
|
|
@@ -177,6 +312,7 @@ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
|
|
|
177
312
|
currency: lotCurrency,
|
|
178
313
|
includedInTotals: false,
|
|
179
314
|
reason: quote?.reason ?? "quote unavailable",
|
|
315
|
+
sparkline: sparklineMap.get(lot.symbol) ?? unavailableSparkline("History unavailable"),
|
|
180
316
|
};
|
|
181
317
|
}
|
|
182
318
|
const resolvedQuoteCurrency = quote.currency ?? quoteCurrency;
|
|
@@ -203,6 +339,7 @@ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
|
|
|
203
339
|
extendedChangePercent: quote.extendedChangePercent,
|
|
204
340
|
extendedAsOf: quote.extendedAsOf,
|
|
205
341
|
stale: quote.stale,
|
|
342
|
+
sparkline: sparklineMap.get(lot.symbol) ?? unavailableSparkline("History unavailable"),
|
|
206
343
|
};
|
|
207
344
|
}
|
|
208
345
|
const marketValue = quote.price * lot.quantity;
|
|
@@ -229,6 +366,7 @@ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
|
|
|
229
366
|
extendedChangePercent: quote.extendedChangePercent,
|
|
230
367
|
extendedAsOf: quote.extendedAsOf,
|
|
231
368
|
stale: quote.stale,
|
|
369
|
+
sparkline: sparklineMap.get(lot.symbol) ?? unavailableSparkline("History unavailable"),
|
|
232
370
|
reason: includedInTotals
|
|
233
371
|
? undefined
|
|
234
372
|
: `No FX conversion from ${lotCurrency === baseCurrency ? quoteCurrency : lotCurrency} to ${baseCurrency}`,
|
|
@@ -265,6 +403,36 @@ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
|
|
|
265
403
|
},
|
|
266
404
|
};
|
|
267
405
|
}
|
|
406
|
+
export async function fetchSparklineSnapshot(symbol) {
|
|
407
|
+
const result = await wrapProvider("yahoo", () => getHistory(symbol, "1d", "5m"));
|
|
408
|
+
if (result.status === "unavailable")
|
|
409
|
+
return unavailableSparkline(result.reason);
|
|
410
|
+
const bars = result.data.filter((bar) => Number.isFinite(bar.close));
|
|
411
|
+
const dataAsOf = bars.at(-1)?.date;
|
|
412
|
+
if (result.stale) {
|
|
413
|
+
return unavailableSparkline("Historical data is stale", dataAsOf, true);
|
|
414
|
+
}
|
|
415
|
+
if (bars.length < 2) {
|
|
416
|
+
return unavailableSparkline("Not enough intraday history", dataAsOf);
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
status: "ok",
|
|
420
|
+
source: "Yahoo Finance",
|
|
421
|
+
points: bars.map((bar) => bar.close),
|
|
422
|
+
fetchedAt: result.timestamp,
|
|
423
|
+
dataAsOf,
|
|
424
|
+
stale: false,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function unavailableSparkline(reason, dataAsOf, stale) {
|
|
428
|
+
return {
|
|
429
|
+
status: "unavailable",
|
|
430
|
+
source: "Yahoo Finance",
|
|
431
|
+
reason,
|
|
432
|
+
dataAsOf,
|
|
433
|
+
stale,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
268
436
|
export async function searchInstrumentCandidates(query) {
|
|
269
437
|
const trimmed = query.trim();
|
|
270
438
|
if (!trimmed)
|
|
@@ -302,6 +470,7 @@ export async function getInstrumentQuoteSnapshot(symbol) {
|
|
|
302
470
|
low: quote.low,
|
|
303
471
|
week52High: quote.week52High,
|
|
304
472
|
week52Low: quote.week52Low,
|
|
473
|
+
marketCap: quote.marketCap,
|
|
305
474
|
fetchedAt: quote.fetchedAt,
|
|
306
475
|
dataAsOf: quote.dataAsOf,
|
|
307
476
|
marketState: quote.marketState,
|
|
@@ -313,7 +482,79 @@ export async function getInstrumentQuoteSnapshot(symbol) {
|
|
|
313
482
|
currency: quote.currency,
|
|
314
483
|
};
|
|
315
484
|
}
|
|
316
|
-
|
|
485
|
+
const INSTRUMENT_OVERVIEW_TTL_MS = 5 * 60_000;
|
|
486
|
+
const instrumentOverviewMemo = new Map();
|
|
487
|
+
export async function getInstrumentOverviewSnapshot(symbol) {
|
|
488
|
+
const normalized = symbol.trim().toUpperCase();
|
|
489
|
+
if (!normalized)
|
|
490
|
+
return { symbol: "", status: "unavailable", reason: "symbol is required" };
|
|
491
|
+
const existing = instrumentOverviewMemo.get(normalized);
|
|
492
|
+
if (existing?.snapshot && Date.now() - existing.fetchedAtMs < INSTRUMENT_OVERVIEW_TTL_MS) {
|
|
493
|
+
return existing.snapshot;
|
|
494
|
+
}
|
|
495
|
+
if (existing?.snapshot) {
|
|
496
|
+
if (!existing.inFlight)
|
|
497
|
+
refreshInstrumentOverview(normalized, existing).catch(() => { });
|
|
498
|
+
return existing.snapshot.status === "ok"
|
|
499
|
+
? { ...existing.snapshot, stale: true }
|
|
500
|
+
: existing.snapshot;
|
|
501
|
+
}
|
|
502
|
+
if (existing?.inFlight)
|
|
503
|
+
return existing.inFlight;
|
|
504
|
+
const entry = existing ?? { snapshot: null, fetchedAtMs: 0, inFlight: null };
|
|
505
|
+
return refreshInstrumentOverview(normalized, entry);
|
|
506
|
+
}
|
|
507
|
+
function refreshInstrumentOverview(normalized, entry) {
|
|
508
|
+
if (entry.inFlight)
|
|
509
|
+
return entry.inFlight;
|
|
510
|
+
const inFlight = loadInstrumentOverviewSnapshot(normalized)
|
|
511
|
+
.then((snapshot) => {
|
|
512
|
+
if (entry.snapshot?.status === "ok" && snapshot.status === "unavailable") {
|
|
513
|
+
return entry.snapshot;
|
|
514
|
+
}
|
|
515
|
+
entry.snapshot = snapshot;
|
|
516
|
+
entry.fetchedAtMs = Date.now();
|
|
517
|
+
return snapshot;
|
|
518
|
+
})
|
|
519
|
+
.finally(() => {
|
|
520
|
+
if (entry.inFlight === inFlight)
|
|
521
|
+
entry.inFlight = null;
|
|
522
|
+
});
|
|
523
|
+
entry.inFlight = inFlight;
|
|
524
|
+
instrumentOverviewMemo.set(normalized, entry);
|
|
525
|
+
return inFlight;
|
|
526
|
+
}
|
|
527
|
+
async function loadInstrumentOverviewSnapshot(normalized) {
|
|
528
|
+
const result = await wrapProvider("yahoo", () => getYahooCompanyOverview(normalized));
|
|
529
|
+
if (result.status === "unavailable") {
|
|
530
|
+
return { symbol: normalized, status: "unavailable", reason: result.reason };
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
symbol: normalized,
|
|
534
|
+
status: "ok",
|
|
535
|
+
name: result.data.name,
|
|
536
|
+
description: result.data.description,
|
|
537
|
+
exchange: result.data.exchange,
|
|
538
|
+
sector: result.data.sector,
|
|
539
|
+
industry: result.data.industry,
|
|
540
|
+
marketCap: result.data.marketCap,
|
|
541
|
+
pe: result.data.pe,
|
|
542
|
+
forwardPe: result.data.forwardPe,
|
|
543
|
+
eps: result.data.eps,
|
|
544
|
+
dividendYield: result.data.dividendYield,
|
|
545
|
+
beta: result.data.beta,
|
|
546
|
+
avgVolume: result.data.avgVolume,
|
|
547
|
+
profitMargin: result.data.profitMargin,
|
|
548
|
+
revenueGrowth: result.data.revenueGrowth,
|
|
549
|
+
week52High: result.data.week52High,
|
|
550
|
+
week52Low: result.data.week52Low,
|
|
551
|
+
stale: result.stale === true,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
export function resetInstrumentOverviewMemoForTests() {
|
|
555
|
+
instrumentOverviewMemo.clear();
|
|
556
|
+
}
|
|
557
|
+
export async function fetchQuoteSnapshot(symbol) {
|
|
317
558
|
const result = await wrapProvider("yahoo", () => getQuote(symbol));
|
|
318
559
|
if (result.status === "unavailable")
|
|
319
560
|
return { status: "unavailable", reason: result.reason };
|
|
@@ -342,6 +583,7 @@ async function fetchQuoteSnapshot(symbol) {
|
|
|
342
583
|
low: result.data.low,
|
|
343
584
|
week52High: result.data.week52High,
|
|
344
585
|
week52Low: result.data.week52Low,
|
|
586
|
+
marketCap: result.data.marketCap,
|
|
345
587
|
fetchedAt: result.timestamp,
|
|
346
588
|
dataAsOf: freshness.providerDataAt,
|
|
347
589
|
marketState: result.data.marketState,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { type ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
export type ModelSetupRequirement = "ready" | "select_model" | "connect_auth";
|
|
3
4
|
export interface ModelSetupProvider {
|
|
4
5
|
id: string;
|
|
@@ -23,18 +24,8 @@ export interface ModelSetupRegistry {
|
|
|
23
24
|
getAvailable(): Model<Api>[];
|
|
24
25
|
hasConfiguredAuth(model: Model<Api>): boolean;
|
|
25
26
|
}
|
|
26
|
-
interface ModelSetupAuthStorage {
|
|
27
|
-
set(provider: string, credential: {
|
|
28
|
-
type: "api_key";
|
|
29
|
-
key: string;
|
|
30
|
-
}): void;
|
|
31
|
-
}
|
|
32
|
-
interface MutableModelSetupRegistry extends ModelSetupRegistry {
|
|
33
|
-
authStorage: ModelSetupAuthStorage;
|
|
34
|
-
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
35
|
-
}
|
|
36
27
|
interface ModelSetupSession {
|
|
37
|
-
|
|
28
|
+
modelRuntime: ModelRuntime;
|
|
38
29
|
model?: Model<Api>;
|
|
39
30
|
setModel(model: Model<Api>): Promise<void>;
|
|
40
31
|
settingsManager: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { persistProviderCredential } from "../../onboarding/connect.js";
|
|
2
3
|
import { getCredentialSource, isApiKeyProvider, PROVIDERS, } from "../../onboarding/providers.js";
|
|
3
4
|
import { validateModelKey, } from "../../onboarding/validate-model-key.js";
|
|
@@ -72,7 +73,7 @@ export function createModelSetupController({ role, getSession, getSessionManager
|
|
|
72
73
|
}
|
|
73
74
|
function buildCurrentModelSetupState() {
|
|
74
75
|
const session = getSession();
|
|
75
|
-
return buildModelSetupState(session.
|
|
76
|
+
return buildModelSetupState(new ModelRegistry(session.modelRuntime), session.model);
|
|
76
77
|
}
|
|
77
78
|
async function handleSaveModelApiKey(providerId, apiKey) {
|
|
78
79
|
ensureWriter();
|
|
@@ -87,9 +88,12 @@ export function createModelSetupController({ role, getSession, getSessionManager
|
|
|
87
88
|
throw new Error(`Key was rejected by ${validation.providerLabel}. The existing configuration was not changed.`);
|
|
88
89
|
}
|
|
89
90
|
const session = getSession();
|
|
90
|
-
session.
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
await session.modelRuntime.login(provider.id, "api_key", {
|
|
92
|
+
prompt: async () => trimmed,
|
|
93
|
+
notify: () => { },
|
|
94
|
+
});
|
|
95
|
+
const modelRegistry = new ModelRegistry(session.modelRuntime);
|
|
96
|
+
const model = findPreferredModel(modelRegistry, provider);
|
|
93
97
|
if (!model) {
|
|
94
98
|
throw new Error(`Saved the ${provider.label} key, but no ${provider.label} models are available yet.`);
|
|
95
99
|
}
|
|
@@ -134,8 +138,8 @@ export function createModelSetupController({ role, getSession, getSessionManager
|
|
|
134
138
|
async function handleSelectModel(provider, modelId) {
|
|
135
139
|
ensureWriter();
|
|
136
140
|
const session = getSession();
|
|
137
|
-
session.
|
|
138
|
-
const model = session.
|
|
141
|
+
await session.modelRuntime.refresh();
|
|
142
|
+
const model = session.modelRuntime.getModel(provider, modelId);
|
|
139
143
|
if (!model)
|
|
140
144
|
throw new Error(`Unknown model: ${provider}/${modelId}`);
|
|
141
145
|
await session.setModel(model);
|
|
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { createAgentSessionRuntime, createAgentSessionServices, getAgentDir, ModelRuntime, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { createOpenCandleSession } from "../../index.js";
|
|
8
8
|
import { assertSupportedNodeVersion } from "../../infra/node-version.js";
|
|
9
9
|
import { createAskUserBridge } from "./ask-user-bridge.js";
|
|
@@ -13,6 +13,8 @@ import { createInitialGuiSessionManager } from "./gui-session-manager.js";
|
|
|
13
13
|
import { createHttpRequestHandler, resolveSessionManagerById } from "./http-routes.js";
|
|
14
14
|
import { createToolInvokeController } from "./invoke-tool.js";
|
|
15
15
|
import { createLocalSessionCoordinator } from "./local-session-coordinator.js";
|
|
16
|
+
import { buildMarketIndicesSnapshot } from "./market-indices-api.js";
|
|
17
|
+
import { MarketIndicesSnapshotStore } from "./market-indices-snapshot-store.js";
|
|
16
18
|
import { buildMarketStateQuoteSnapshot } from "./market-state-api.js";
|
|
17
19
|
import { createModelSetupController } from "./model-setup.js";
|
|
18
20
|
import { isTrustedPrivateApiRequest } from "./private-api-access.js";
|
|
@@ -33,8 +35,10 @@ const localCoordinatorEndpoint = `http://${coordinatorEndpointHost(host)}:${port
|
|
|
33
35
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
34
36
|
const webDist = resolveGuiWebDist(__dirname);
|
|
35
37
|
const agentDir = getAgentDir();
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
+
const modelRuntime = await ModelRuntime.create({
|
|
39
|
+
authPath: resolve(agentDir, "auth.json"),
|
|
40
|
+
modelsPath: resolve(agentDir, "models.json"),
|
|
41
|
+
});
|
|
38
42
|
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
39
43
|
const initialSessionManager = createInitialGuiSessionManager(cwd);
|
|
40
44
|
let sessionManager = initialSessionManager;
|
|
@@ -56,15 +60,13 @@ const runtime = await createAgentSessionRuntime(async (opts) => {
|
|
|
56
60
|
const services = await createAgentSessionServices({
|
|
57
61
|
cwd: opts.cwd,
|
|
58
62
|
agentDir: opts.agentDir,
|
|
59
|
-
authStorage,
|
|
60
63
|
settingsManager,
|
|
61
|
-
|
|
64
|
+
modelRuntime,
|
|
62
65
|
});
|
|
63
66
|
const result = await createOpenCandleSession({
|
|
64
67
|
cwd: opts.cwd,
|
|
65
68
|
agentDir: opts.agentDir,
|
|
66
|
-
|
|
67
|
-
modelRegistry,
|
|
69
|
+
modelRuntime,
|
|
68
70
|
settingsManager,
|
|
69
71
|
sessionManager: opts.sessionManager,
|
|
70
72
|
askUserHandler: askUserBridge.ask,
|
|
@@ -100,6 +102,7 @@ const heartbeat = setInterval(() => {
|
|
|
100
102
|
const backgroundQuoteRefreshes = new BackgroundQuoteRefreshes();
|
|
101
103
|
const localSessionCoordinator = createLocalSessionCoordinator();
|
|
102
104
|
const quoteSnapshotStore = new QuoteSnapshotStore(() => buildMarketStateQuoteSnapshot());
|
|
105
|
+
const indicesSnapshotStore = new MarketIndicesSnapshotStore(() => buildMarketIndicesSnapshot());
|
|
103
106
|
quotePoller = createBackgroundQuotePoller({
|
|
104
107
|
getClientCount: () => wsHub.getClientCount(),
|
|
105
108
|
getSessionManager: () => sessionManager,
|
|
@@ -200,8 +203,7 @@ const httpRequestHandler = createHttpRequestHandler({
|
|
|
200
203
|
createSessionForManager: async (targetSessionManager) => createOpenCandleSession({
|
|
201
204
|
cwd,
|
|
202
205
|
agentDir,
|
|
203
|
-
|
|
204
|
-
modelRegistry,
|
|
206
|
+
modelRuntime,
|
|
205
207
|
settingsManager,
|
|
206
208
|
sessionManager: targetSessionManager,
|
|
207
209
|
askUserHandler: askUserBridge.askForSession(targetSessionManager.getSessionId()),
|
|
@@ -211,6 +213,7 @@ const httpRequestHandler = createHttpRequestHandler({
|
|
|
211
213
|
sessionActionsController,
|
|
212
214
|
toolInvokeController,
|
|
213
215
|
quoteSnapshotStore,
|
|
216
|
+
indicesSnapshotStore,
|
|
214
217
|
localSessionCoordinator,
|
|
215
218
|
});
|
|
216
219
|
const server = createServer((req, res) => {
|
|
@@ -20,13 +20,23 @@ export declare function buildCatalog(): {
|
|
|
20
20
|
}[];
|
|
21
21
|
}[];
|
|
22
22
|
providers: ({
|
|
23
|
-
|
|
23
|
+
id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
|
|
24
|
+
kind: "api-key" | "external-tool" | "public-http";
|
|
25
|
+
displayName: string;
|
|
26
|
+
category: import("../../onboarding/providers.js").ProviderCategory;
|
|
27
|
+
tier: import("../../onboarding/providers.js").ProviderTier;
|
|
28
|
+
aliases: readonly string[];
|
|
29
|
+
unlocks: readonly string[];
|
|
30
|
+
fallbackDescription: string | null;
|
|
31
|
+
instructionsHint: string;
|
|
32
|
+
source: "absent" | "env" | "file";
|
|
24
33
|
configured: boolean;
|
|
25
34
|
maskedKeyHint: string | undefined;
|
|
26
|
-
status: "
|
|
35
|
+
status: "absent" | "env" | "file";
|
|
27
36
|
signupUrl: string;
|
|
28
37
|
freeTier: boolean;
|
|
29
38
|
envVar: string;
|
|
39
|
+
} | {
|
|
30
40
|
id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
|
|
31
41
|
kind: "api-key" | "external-tool" | "public-http";
|
|
32
42
|
displayName: string;
|
|
@@ -36,7 +46,6 @@ export declare function buildCatalog(): {
|
|
|
36
46
|
unlocks: readonly string[];
|
|
37
47
|
fallbackDescription: string | null;
|
|
38
48
|
instructionsHint: string;
|
|
39
|
-
} | {
|
|
40
49
|
status: string;
|
|
41
50
|
statusDetail: {
|
|
42
51
|
providerId: import("../../onboarding/providers.js").ExternalToolProviderId;
|
|
@@ -52,6 +61,7 @@ export declare function buildCatalog(): {
|
|
|
52
61
|
installCmd: string;
|
|
53
62
|
sessionSource: "browser-cookies";
|
|
54
63
|
supportedBrowsers: readonly string[] | undefined;
|
|
64
|
+
} | {
|
|
55
65
|
id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
|
|
56
66
|
kind: "api-key" | "external-tool" | "public-http";
|
|
57
67
|
displayName: string;
|
|
@@ -61,12 +71,12 @@ export declare function buildCatalog(): {
|
|
|
61
71
|
unlocks: readonly string[];
|
|
62
72
|
fallbackDescription: string | null;
|
|
63
73
|
instructionsHint: string;
|
|
64
|
-
} | {
|
|
65
74
|
status: string;
|
|
66
75
|
binary: string;
|
|
67
76
|
installCmd: string;
|
|
68
77
|
sessionSource: "browser-cookies";
|
|
69
78
|
supportedBrowsers: readonly string[] | undefined;
|
|
79
|
+
} | {
|
|
70
80
|
id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
|
|
71
81
|
kind: "api-key" | "external-tool" | "public-http";
|
|
72
82
|
displayName: string;
|
|
@@ -76,18 +86,8 @@ export declare function buildCatalog(): {
|
|
|
76
86
|
unlocks: readonly string[];
|
|
77
87
|
fallbackDescription: string | null;
|
|
78
88
|
instructionsHint: string;
|
|
79
|
-
} | {
|
|
80
89
|
status: string;
|
|
81
90
|
probeUrl: string;
|
|
82
|
-
id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
|
|
83
|
-
kind: "api-key" | "external-tool" | "public-http";
|
|
84
|
-
displayName: string;
|
|
85
|
-
category: import("../../onboarding/providers.js").ProviderCategory;
|
|
86
|
-
tier: import("../../onboarding/providers.js").ProviderTier;
|
|
87
|
-
aliases: readonly string[];
|
|
88
|
-
unlocks: readonly string[];
|
|
89
|
-
fallbackDescription: string | null;
|
|
90
|
-
instructionsHint: string;
|
|
91
91
|
})[];
|
|
92
92
|
};
|
|
93
93
|
export declare function setToolEnabled(toolName: string, enabled: boolean): void;
|
|
@@ -64,7 +64,7 @@ export function createWsHub({ role, lock, cwd, sessionDir, getSession, getSessio
|
|
|
64
64
|
client.send({ type: "catalog", catalog: buildCatalog() });
|
|
65
65
|
break;
|
|
66
66
|
case "model.setup.refresh":
|
|
67
|
-
getSession().
|
|
67
|
+
await getSession().modelRuntime.refresh();
|
|
68
68
|
broadcastModelSetup();
|
|
69
69
|
break;
|
|
70
70
|
case "model.setup.save_api_key":
|
package/dist/infra/cache.d.ts
CHANGED
|
@@ -31,7 +31,9 @@ export declare const cache: Cache;
|
|
|
31
31
|
export declare const TTL: {
|
|
32
32
|
readonly QUOTE: 60000;
|
|
33
33
|
readonly HISTORY: 3600000;
|
|
34
|
+
readonly CANDLES: 3600000;
|
|
34
35
|
readonly FUNDAMENTALS: 86400000;
|
|
36
|
+
readonly FINANCIAL_REPORTS: 86400000;
|
|
35
37
|
readonly MACRO: 3600000;
|
|
36
38
|
readonly SENTIMENT: 300000;
|
|
37
39
|
readonly OPTIONS_CHAIN: 120000;
|
|
@@ -44,7 +46,9 @@ export declare const TTL: {
|
|
|
44
46
|
export declare const STALE_LIMIT: {
|
|
45
47
|
readonly QUOTE: number;
|
|
46
48
|
readonly HISTORY: number;
|
|
49
|
+
readonly CANDLES: number;
|
|
47
50
|
readonly FUNDAMENTALS: number;
|
|
51
|
+
readonly FINANCIAL_REPORTS: number;
|
|
48
52
|
readonly MACRO: number;
|
|
49
53
|
readonly SENTIMENT: 3600000;
|
|
50
54
|
readonly OPTIONS_CHAIN: number;
|
package/dist/infra/cache.js
CHANGED
|
@@ -69,7 +69,9 @@ export const cache = new Cache();
|
|
|
69
69
|
export const TTL = {
|
|
70
70
|
QUOTE: 60_000, // 1 minute
|
|
71
71
|
HISTORY: 3_600_000, // 1 hour
|
|
72
|
+
CANDLES: 3_600_000, // 1 hour
|
|
72
73
|
FUNDAMENTALS: 86_400_000, // 24 hours
|
|
74
|
+
FINANCIAL_REPORTS: 86_400_000, // 24 hours
|
|
73
75
|
MACRO: 3_600_000, // 1 hour
|
|
74
76
|
SENTIMENT: 300_000, // 5 minutes
|
|
75
77
|
OPTIONS_CHAIN: 120_000, // 2 minutes
|
|
@@ -83,7 +85,9 @@ export const TTL = {
|
|
|
83
85
|
export const STALE_LIMIT = {
|
|
84
86
|
QUOTE: 15 * 60_000, // 15 minutes
|
|
85
87
|
HISTORY: 24 * 3_600_000, // 24 hours
|
|
88
|
+
CANDLES: 24 * 3_600_000, // 24 hours
|
|
86
89
|
FUNDAMENTALS: 7 * 86_400_000, // 7 days
|
|
90
|
+
FINANCIAL_REPORTS: 7 * 86_400_000, // 7 days
|
|
87
91
|
MACRO: 24 * 3_600_000, // 24 hours
|
|
88
92
|
SENTIMENT: 3_600_000, // 1 hour
|
|
89
93
|
OPTIONS_CHAIN: 30 * 60_000, // 30 minutes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const LSE_MONTHLY_BYTE_CAP = 53687091200;
|
|
2
|
+
export declare const LSE_SOFT_BYTE_THRESHOLD = 42949672960;
|
|
3
|
+
export interface LseByteBudgetUsage {
|
|
4
|
+
month: string;
|
|
5
|
+
bytesUsed: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function recordBytes(n: number): void;
|
|
8
|
+
export declare function getUsage(): LseByteBudgetUsage;
|
|
9
|
+
export declare function isOverSoftThreshold(): boolean;
|
|
10
|
+
export declare function markExhausted(): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { ensureParentDir, resolveOpenCandlePath } from "./opencandle-paths.js";
|
|
3
|
+
export const LSE_MONTHLY_BYTE_CAP = 53_687_091_200;
|
|
4
|
+
export const LSE_SOFT_BYTE_THRESHOLD = 42_949_672_960;
|
|
5
|
+
function currentMonth() {
|
|
6
|
+
return new Date().toISOString().slice(0, 7);
|
|
7
|
+
}
|
|
8
|
+
function budgetPath() {
|
|
9
|
+
return resolveOpenCandlePath("lse-byte-budget.json");
|
|
10
|
+
}
|
|
11
|
+
function readUsage() {
|
|
12
|
+
const month = currentMonth();
|
|
13
|
+
const path = budgetPath();
|
|
14
|
+
if (!existsSync(path))
|
|
15
|
+
return { month, bytesUsed: 0 };
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
18
|
+
const bytesUsed = parsed.bytesUsed;
|
|
19
|
+
if (parsed.month !== month || typeof bytesUsed !== "number" || !Number.isFinite(bytesUsed)) {
|
|
20
|
+
return { month, bytesUsed: 0 };
|
|
21
|
+
}
|
|
22
|
+
return { month, bytesUsed: bytesUsed < 0 ? 0 : bytesUsed };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { month, bytesUsed: 0 };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeUsage(usage) {
|
|
29
|
+
const path = budgetPath();
|
|
30
|
+
ensureParentDir(path);
|
|
31
|
+
writeFileSync(path, `${JSON.stringify(usage, null, 2)}\n`, "utf-8");
|
|
32
|
+
}
|
|
33
|
+
export function recordBytes(n) {
|
|
34
|
+
if (!Number.isFinite(n) || n < 0)
|
|
35
|
+
return;
|
|
36
|
+
const usage = readUsage();
|
|
37
|
+
writeUsage({ ...usage, bytesUsed: usage.bytesUsed + n });
|
|
38
|
+
}
|
|
39
|
+
export function getUsage() {
|
|
40
|
+
return readUsage();
|
|
41
|
+
}
|
|
42
|
+
export function isOverSoftThreshold() {
|
|
43
|
+
return readUsage().bytesUsed >= LSE_SOFT_BYTE_THRESHOLD;
|
|
44
|
+
}
|
|
45
|
+
export function markExhausted() {
|
|
46
|
+
writeUsage({ month: currentMonth(), bytesUsed: LSE_MONTHLY_BYTE_CAP });
|
|
47
|
+
}
|
|
@@ -50,3 +50,4 @@ rateLimiter.configure("tradingview", 5, 1); // 5 burst, 1 req/s sustained
|
|
|
50
50
|
rateLimiter.configure("polymarket", 10, 5); // 10 burst, 5 req/s sustained
|
|
51
51
|
// SEC EDGAR fair-access guideline is 10 req/s; stay below it.
|
|
52
52
|
rateLimiter.configure("sec_edgar", 5, 5); // 5 burst, 5 req/s sustained
|
|
53
|
+
rateLimiter.configure("lse", 100, 1.66); // 100 req/min (free tier)
|