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,43 @@
|
|
|
1
|
+
export class HistorySnapshotStore {
|
|
2
|
+
maxAgeMs;
|
|
3
|
+
now;
|
|
4
|
+
entries = new Map();
|
|
5
|
+
constructor(maxAgeMs = 60_000, now = Date.now) {
|
|
6
|
+
this.maxAgeMs = maxAgeMs;
|
|
7
|
+
this.now = now;
|
|
8
|
+
}
|
|
9
|
+
get(key, build) {
|
|
10
|
+
const existing = this.entries.get(key);
|
|
11
|
+
if (existing?.hasValue && this.now() - existing.fetchedAtMs < this.maxAgeMs) {
|
|
12
|
+
return Promise.resolve(existing.value);
|
|
13
|
+
}
|
|
14
|
+
if (existing?.inFlight)
|
|
15
|
+
return existing.inFlight;
|
|
16
|
+
const entry = existing ??
|
|
17
|
+
{ hasValue: false, fetchedAtMs: 0, inFlight: null };
|
|
18
|
+
let buildPromise;
|
|
19
|
+
try {
|
|
20
|
+
buildPromise = build();
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
buildPromise = Promise.reject(error);
|
|
24
|
+
}
|
|
25
|
+
const inFlight = buildPromise
|
|
26
|
+
.then((value) => {
|
|
27
|
+
entry.value = value;
|
|
28
|
+
entry.hasValue = true;
|
|
29
|
+
entry.fetchedAtMs = this.now();
|
|
30
|
+
return value;
|
|
31
|
+
})
|
|
32
|
+
.finally(() => {
|
|
33
|
+
if (entry.inFlight === inFlight)
|
|
34
|
+
entry.inFlight = null;
|
|
35
|
+
});
|
|
36
|
+
entry.inFlight = inFlight;
|
|
37
|
+
this.entries.set(key, entry);
|
|
38
|
+
return inFlight;
|
|
39
|
+
}
|
|
40
|
+
clear() {
|
|
41
|
+
this.entries.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
|
|
|
2
2
|
import { type AgentSession, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { ToolInvokeController } from "./invoke-tool.js";
|
|
4
4
|
import type { LocalSessionCoordinator, SessionActionEnvelope } from "./local-session-coordinator.js";
|
|
5
|
+
import type { MarketIndicesSnapshotStore } from "./market-indices-snapshot-store.js";
|
|
5
6
|
import { type ModelSetupController } from "./model-setup.js";
|
|
6
7
|
import type { QuoteSnapshotStore } from "./quote-snapshot-store.js";
|
|
7
8
|
import { type SessionActionsController } from "./session-actions.js";
|
|
@@ -29,6 +30,7 @@ interface GuiHttpRouteOptions {
|
|
|
29
30
|
sessionActionsController: SessionActionsController;
|
|
30
31
|
toolInvokeController: ToolInvokeController;
|
|
31
32
|
quoteSnapshotStore: QuoteSnapshotStore;
|
|
33
|
+
indicesSnapshotStore: MarketIndicesSnapshotStore;
|
|
32
34
|
localSessionCoordinator?: LocalSessionCoordinator;
|
|
33
35
|
}
|
|
34
36
|
export interface ChatRunImageInput {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createReadStream, existsSync } from "node:fs";
|
|
2
2
|
import { extname, join, resolve } from "node:path";
|
|
3
|
-
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { ModelRegistry, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { buildDoctorReport } from "../../doctor/report.js";
|
|
5
5
|
import { MarketStateService } from "../../market-state/service.js";
|
|
6
6
|
import { formatLatestReportSummary, formatPortfolioSummary, formatWatchlistSummary, } from "../../market-state/summaries.js";
|
|
@@ -9,7 +9,7 @@ import { probeProviderStatus } from "../../onboarding/provider-status.js";
|
|
|
9
9
|
import { clearPendingSessionAction, hasAcceptedSessionAction, hasPendingSessionAction, recordAcceptedSessionAction, recordPendingSessionAction, } from "../../pi/session-action-dedupe.js";
|
|
10
10
|
import { sessionEntriesToChatEvents } from "./chat-event-adapter.js";
|
|
11
11
|
import { createLiveChatEventAdapter } from "./live-chat-event-adapter.js";
|
|
12
|
-
import { buildMarketStateSnapshot, getInstrumentQuoteSnapshot, getSavedMarketStateSymbols, searchInstrumentCandidates, } from "./market-state-api.js";
|
|
12
|
+
import { buildMarketStateSnapshot, getInstrumentHistorySnapshot, getInstrumentOverviewSnapshot, getInstrumentQuoteSnapshot, getSavedMarketStateSymbols, searchInstrumentCandidates, } from "./market-state-api.js";
|
|
13
13
|
import { buildModelSetupState } from "./model-setup.js";
|
|
14
14
|
import { isTrustedPrivateApiRequest, privateApiCookieHeader } from "./private-api-access.js";
|
|
15
15
|
import { projectDashboard } from "./projector.js";
|
|
@@ -84,9 +84,10 @@ export function createHttpRequestHandler(options) {
|
|
|
84
84
|
if (url.pathname === "/api/model-setup/refresh" && req.method === "POST") {
|
|
85
85
|
if (!allowTrustedGuiRequest(req, res, "Model setup API", options))
|
|
86
86
|
return;
|
|
87
|
-
options
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
await handleTrustedGuiMutation(req, res, options, async () => {
|
|
88
|
+
await options.getSession().modelRuntime.refresh();
|
|
89
|
+
options.wsHub.broadcastModelSetup();
|
|
90
|
+
});
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
92
93
|
if (url.pathname === "/api/model-setup/api-key" && req.method === "POST") {
|
|
@@ -128,6 +129,12 @@ export function createHttpRequestHandler(options) {
|
|
|
128
129
|
writeJson(res, await options.quoteSnapshotStore.get());
|
|
129
130
|
return;
|
|
130
131
|
}
|
|
132
|
+
if (url.pathname === "/api/market-state/indices" && req.method === "GET") {
|
|
133
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API", options))
|
|
134
|
+
return;
|
|
135
|
+
writeJson(res, await options.indicesSnapshotStore.get());
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
131
138
|
if (url.pathname === "/api/doctor" && req.method === "GET") {
|
|
132
139
|
if (!allowTrustedGuiRequest(req, res, "Diagnostics API", options))
|
|
133
140
|
return;
|
|
@@ -143,7 +150,7 @@ export function createHttpRequestHandler(options) {
|
|
|
143
150
|
reachable: true,
|
|
144
151
|
healthEndpoint: `http://${options.host}:${options.port}/health`,
|
|
145
152
|
},
|
|
146
|
-
modelSetup: buildModelSetupState(session.
|
|
153
|
+
modelSetup: buildModelSetupState(new ModelRegistry(session.modelRuntime), session.model),
|
|
147
154
|
}));
|
|
148
155
|
return;
|
|
149
156
|
}
|
|
@@ -159,6 +166,23 @@ export function createHttpRequestHandler(options) {
|
|
|
159
166
|
writeJson(res, await getInstrumentQuoteSnapshot(url.searchParams.get("symbol") ?? ""));
|
|
160
167
|
return;
|
|
161
168
|
}
|
|
169
|
+
if (url.pathname === "/api/instruments/overview" && req.method === "GET") {
|
|
170
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API", options))
|
|
171
|
+
return;
|
|
172
|
+
writeJson(res, await getInstrumentOverviewSnapshot(url.searchParams.get("symbol") ?? ""));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (url.pathname === "/api/instruments/history" && req.method === "GET") {
|
|
176
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API", options))
|
|
177
|
+
return;
|
|
178
|
+
const snapshot = await getInstrumentHistorySnapshot(url.searchParams.get("symbol") ?? "", url.searchParams.get("range") ?? "1D", url.searchParams.get("interval") ?? undefined);
|
|
179
|
+
if (snapshot.status === "invalid_request") {
|
|
180
|
+
writeJson(res, snapshot, 400);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
writeJson(res, snapshot);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
162
186
|
if (url.pathname === "/api/diagnostics/twitter-cli" && req.method === "GET") {
|
|
163
187
|
if (!allowTrustedGuiRequest(req, res, "Diagnostics API", options))
|
|
164
188
|
return;
|
|
@@ -529,6 +553,7 @@ async function streamAcceptedSseChatRun({ res, options, activeRunSessionIds, tar
|
|
|
529
553
|
startSeq: seq,
|
|
530
554
|
emit: (event) => writeSse(res, event),
|
|
531
555
|
originalPrompt: prompt,
|
|
556
|
+
dispatchedPrompt,
|
|
532
557
|
originalAttachments: inputAttachmentLabels,
|
|
533
558
|
});
|
|
534
559
|
const observation = createPromptObservation();
|
|
@@ -563,7 +588,7 @@ async function streamAcceptedSseChatRun({ res, options, activeRunSessionIds, tar
|
|
|
563
588
|
});
|
|
564
589
|
try {
|
|
565
590
|
recordPendingSessionAction(runSessionManager, actionId);
|
|
566
|
-
const modelSetup = buildModelSetupState(runSession.
|
|
591
|
+
const modelSetup = buildModelSetupState(new ModelRegistry(runSession.modelRuntime), runSession.model);
|
|
567
592
|
if (!prompt.startsWith("/") && modelSetup.requirement !== "ready") {
|
|
568
593
|
runSessionManager.appendMessage({ role: "user", content: prompt, timestamp: Date.now() });
|
|
569
594
|
recordAcceptedAction();
|
|
@@ -10,6 +10,8 @@ export interface LiveChatEventAdapterOptions {
|
|
|
10
10
|
* message with an expanded prompt; the live view renders this instead.
|
|
11
11
|
*/
|
|
12
12
|
originalPrompt?: string;
|
|
13
|
+
/** Prompt sent to Pi before any workflow input transform. */
|
|
14
|
+
dispatchedPrompt?: string;
|
|
13
15
|
originalAttachments?: MessageAttachmentChip[];
|
|
14
16
|
}
|
|
15
17
|
export interface LiveChatEventAdapter {
|
|
@@ -42,21 +42,39 @@ export function createLiveChatEventAdapter(options) {
|
|
|
42
42
|
const message = event.message;
|
|
43
43
|
if (message.role === "user") {
|
|
44
44
|
const messageId = `${options.runId}-user-${++userCount}`;
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
messageId,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
options.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
const promptText = messageText(message.content);
|
|
46
|
+
const text = userCount === 1 && options.originalPrompt ? options.originalPrompt : promptText;
|
|
47
|
+
const transformedFirstPrompt = userCount === 1 &&
|
|
48
|
+
options.dispatchedPrompt !== undefined &&
|
|
49
|
+
promptText.trim() !== options.dispatchedPrompt.trim();
|
|
50
|
+
const workflowStep = transformedFirstPrompt || userCount > 1;
|
|
51
|
+
if (userCount === 1 || !workflowStep) {
|
|
52
|
+
emit({ type: "message.created", runId: options.runId, messageId, role: "user" });
|
|
53
|
+
emit({
|
|
54
|
+
type: "message.completed",
|
|
55
|
+
runId: options.runId,
|
|
56
|
+
messageId,
|
|
57
|
+
content: userMessageContent(message.content, text),
|
|
58
|
+
...(userCount === 1 &&
|
|
59
|
+
options.originalAttachments &&
|
|
60
|
+
options.originalAttachments.length > 0
|
|
61
|
+
? { attachments: options.originalAttachments }
|
|
62
|
+
: {}),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
if (workflowStep) {
|
|
66
|
+
emit({
|
|
67
|
+
type: "custom.message",
|
|
68
|
+
messageId: `${options.runId}-workflow-step-${userCount}`,
|
|
69
|
+
customType: "opencandle-workflow-step",
|
|
70
|
+
content: [{ type: "text", text: promptText }],
|
|
71
|
+
details: {
|
|
72
|
+
label: "Workflow step",
|
|
73
|
+
stage: "workflow",
|
|
74
|
+
step: userCount,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
60
78
|
return;
|
|
61
79
|
}
|
|
62
80
|
if (message.role === "assistant") {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type MarketSparklineSnapshot } from "./market-state-api.js";
|
|
2
|
+
export declare const MARKET_INDEX_SYMBOLS: readonly ["^GSPC", "^IXIC", "^DJI", "BTC-USD"];
|
|
3
|
+
export interface MarketIndexSnapshotEntry {
|
|
4
|
+
symbol: (typeof MARKET_INDEX_SYMBOLS)[number];
|
|
5
|
+
name?: string;
|
|
6
|
+
status: "ok" | "unavailable";
|
|
7
|
+
reason?: string;
|
|
8
|
+
price?: number;
|
|
9
|
+
change?: number;
|
|
10
|
+
changePercent?: number;
|
|
11
|
+
currency?: string | null;
|
|
12
|
+
marketState?: "PRE" | "REGULAR" | "POST" | "CLOSED";
|
|
13
|
+
dataAsOf?: string;
|
|
14
|
+
stale?: boolean;
|
|
15
|
+
sparkline?: MarketSparklineSnapshot;
|
|
16
|
+
}
|
|
17
|
+
export interface MarketIndicesSnapshot {
|
|
18
|
+
generatedAt: string;
|
|
19
|
+
indices: MarketIndexSnapshotEntry[];
|
|
20
|
+
}
|
|
21
|
+
export declare function buildMarketIndicesSnapshot(): Promise<MarketIndicesSnapshot>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { fetchQuoteSnapshot, fetchSparklineSnapshot, } from "./market-state-api.js";
|
|
2
|
+
export const MARKET_INDEX_SYMBOLS = ["^GSPC", "^IXIC", "^DJI", "BTC-USD"];
|
|
3
|
+
export async function buildMarketIndicesSnapshot() {
|
|
4
|
+
const indices = [];
|
|
5
|
+
for (const symbol of MARKET_INDEX_SYMBOLS) {
|
|
6
|
+
const quote = await fetchQuoteSnapshot(symbol);
|
|
7
|
+
if (quote.status === "unavailable") {
|
|
8
|
+
indices.push({ symbol, status: "unavailable", reason: quote.reason });
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
indices.push({
|
|
12
|
+
symbol,
|
|
13
|
+
name: quote.name,
|
|
14
|
+
status: "ok",
|
|
15
|
+
price: quote.price,
|
|
16
|
+
change: quote.change,
|
|
17
|
+
changePercent: quote.changePercent,
|
|
18
|
+
currency: quote.currency,
|
|
19
|
+
marketState: quote.marketState,
|
|
20
|
+
dataAsOf: quote.dataAsOf,
|
|
21
|
+
stale: quote.stale,
|
|
22
|
+
sparkline: await fetchSparklineSnapshot(symbol),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return { generatedAt: new Date().toISOString(), indices };
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MarketIndicesSnapshot } from "./market-indices-api.js";
|
|
2
|
+
export declare class MarketIndicesSnapshotStore {
|
|
3
|
+
private readonly build;
|
|
4
|
+
private readonly maxAgeMs;
|
|
5
|
+
private readonly now;
|
|
6
|
+
private snapshot;
|
|
7
|
+
private fetchedAtMs;
|
|
8
|
+
private inFlight;
|
|
9
|
+
constructor(build: () => Promise<MarketIndicesSnapshot>, maxAgeMs?: number, now?: () => number);
|
|
10
|
+
get(): Promise<MarketIndicesSnapshot>;
|
|
11
|
+
private refresh;
|
|
12
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class MarketIndicesSnapshotStore {
|
|
2
|
+
build;
|
|
3
|
+
maxAgeMs;
|
|
4
|
+
now;
|
|
5
|
+
snapshot = null;
|
|
6
|
+
fetchedAtMs = 0;
|
|
7
|
+
inFlight = null;
|
|
8
|
+
constructor(build, maxAgeMs = 60_000, now = Date.now) {
|
|
9
|
+
this.build = build;
|
|
10
|
+
this.maxAgeMs = maxAgeMs;
|
|
11
|
+
this.now = now;
|
|
12
|
+
}
|
|
13
|
+
async get() {
|
|
14
|
+
if (this.snapshot == null)
|
|
15
|
+
return this.refresh();
|
|
16
|
+
if (this.now() - this.fetchedAtMs >= this.maxAgeMs) {
|
|
17
|
+
this.refresh().catch(() => { });
|
|
18
|
+
return {
|
|
19
|
+
...this.snapshot,
|
|
20
|
+
indices: this.snapshot.indices.map((entry) => ({ ...entry, stale: true })),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return this.snapshot;
|
|
24
|
+
}
|
|
25
|
+
refresh() {
|
|
26
|
+
if (this.inFlight)
|
|
27
|
+
return this.inFlight;
|
|
28
|
+
const inFlight = this.build()
|
|
29
|
+
.then((snapshot) => {
|
|
30
|
+
if (this.snapshot && snapshot.indices.some((entry) => entry.status === "unavailable")) {
|
|
31
|
+
const validRefreshCount = snapshot.indices.filter((entry) => entry.status === "ok").length;
|
|
32
|
+
if (validRefreshCount === 0)
|
|
33
|
+
return this.snapshot;
|
|
34
|
+
const priorBySymbol = new Map(this.snapshot.indices.map((entry) => [entry.symbol, entry]));
|
|
35
|
+
snapshot = {
|
|
36
|
+
...snapshot,
|
|
37
|
+
indices: snapshot.indices.map((entry) => {
|
|
38
|
+
if (entry.status === "ok")
|
|
39
|
+
return entry;
|
|
40
|
+
const prior = priorBySymbol.get(entry.symbol);
|
|
41
|
+
return prior?.status === "ok" ? { ...prior, stale: true } : entry;
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
this.snapshot = snapshot;
|
|
46
|
+
this.fetchedAtMs = this.now();
|
|
47
|
+
return snapshot;
|
|
48
|
+
})
|
|
49
|
+
.finally(() => {
|
|
50
|
+
if (this.inFlight === inFlight)
|
|
51
|
+
this.inFlight = null;
|
|
52
|
+
});
|
|
53
|
+
this.inFlight = inFlight;
|
|
54
|
+
return inFlight;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
2
|
import { searchYahooInstruments } from "../../market-state/resolve.js";
|
|
3
3
|
import { MarketStateService } from "../../market-state/service.js";
|
|
4
|
+
import { HISTORY_INTERVALS, type HISTORY_RANGES } from "../../tools/market/stock-history.js";
|
|
5
|
+
type HistoryRange = (typeof HISTORY_RANGES)[number];
|
|
6
|
+
type HistoryInterval = (typeof HISTORY_INTERVALS)[number];
|
|
7
|
+
export interface ResolvedHistoryRange {
|
|
8
|
+
range: HistoryRange;
|
|
9
|
+
interval: HistoryInterval;
|
|
10
|
+
}
|
|
11
|
+
export interface InvalidHistoryRequest {
|
|
12
|
+
status: "invalid_request";
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
export interface InstrumentHistoryBar {
|
|
16
|
+
time: number;
|
|
17
|
+
open: number;
|
|
18
|
+
high: number;
|
|
19
|
+
low: number;
|
|
20
|
+
close: number;
|
|
21
|
+
volume: number;
|
|
22
|
+
}
|
|
23
|
+
export interface InstrumentHistorySnapshotOk {
|
|
24
|
+
status: "ok";
|
|
25
|
+
symbol: string;
|
|
26
|
+
range: string;
|
|
27
|
+
interval: HistoryInterval;
|
|
28
|
+
source: InstrumentHistorySource;
|
|
29
|
+
fetchedAt: string;
|
|
30
|
+
dataAsOf?: string;
|
|
31
|
+
stale: boolean;
|
|
32
|
+
prevClose: number | null;
|
|
33
|
+
bars: InstrumentHistoryBar[];
|
|
34
|
+
}
|
|
35
|
+
export interface InstrumentHistorySnapshotUnavailable {
|
|
36
|
+
status: "unavailable";
|
|
37
|
+
reason: string;
|
|
38
|
+
dataAsOf?: string;
|
|
39
|
+
stale?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export type InstrumentHistorySnapshot = InstrumentHistorySnapshotOk | InstrumentHistorySnapshotUnavailable | InvalidHistoryRequest;
|
|
42
|
+
type InstrumentHistorySource = "Yahoo Finance" | "Alpha Vantage" | "London Strategic Edge";
|
|
43
|
+
export declare function resolveHistoryRange(rangeLabel: string, intervalOverride?: string): ResolvedHistoryRange | InvalidHistoryRequest;
|
|
44
|
+
export declare function getInstrumentHistorySnapshot(symbol: string, rangeLabel?: string, intervalOverride?: string): Promise<InstrumentHistorySnapshot>;
|
|
45
|
+
export declare function resetInstrumentHistoryMemoForTests(): void;
|
|
46
|
+
export type MarketSparklineSnapshot = {
|
|
47
|
+
status: "ok";
|
|
48
|
+
source: "Yahoo Finance";
|
|
49
|
+
points: number[];
|
|
50
|
+
fetchedAt: string;
|
|
51
|
+
dataAsOf?: string;
|
|
52
|
+
stale: false;
|
|
53
|
+
} | {
|
|
54
|
+
status: "unavailable";
|
|
55
|
+
source: "Yahoo Finance";
|
|
56
|
+
reason: string;
|
|
57
|
+
dataAsOf?: string;
|
|
58
|
+
stale?: boolean;
|
|
59
|
+
};
|
|
4
60
|
export interface MarketStateSnapshot {
|
|
5
61
|
instruments: Array<NonNullable<ReturnType<MarketStateService["getInstrument"]>>>;
|
|
6
62
|
watchlists: ReturnType<MarketStateService["listWatchlists"]>;
|
|
@@ -42,6 +98,7 @@ export interface MarketStateQuoteSnapshot {
|
|
|
42
98
|
extendedAsOf?: string;
|
|
43
99
|
stale?: boolean;
|
|
44
100
|
reason?: string;
|
|
101
|
+
sparkline: MarketSparklineSnapshot;
|
|
45
102
|
}>;
|
|
46
103
|
portfolioQuotes: Array<{
|
|
47
104
|
lotId: number;
|
|
@@ -68,6 +125,7 @@ export interface MarketStateQuoteSnapshot {
|
|
|
68
125
|
extendedAsOf?: string;
|
|
69
126
|
stale?: boolean;
|
|
70
127
|
reason?: string;
|
|
128
|
+
sparkline: MarketSparklineSnapshot;
|
|
71
129
|
}>;
|
|
72
130
|
portfolioSummary: {
|
|
73
131
|
portfolioId: number;
|
|
@@ -104,6 +162,7 @@ export declare function createSavedSymbolsMemo(load: () => string[], options?: S
|
|
|
104
162
|
export declare const getSavedMarketStateSymbols: () => string[];
|
|
105
163
|
export declare function buildMarketStateSnapshot(db?: Database.Database): MarketStateSnapshot;
|
|
106
164
|
export declare function buildMarketStateQuoteSnapshot(db?: Database.Database): Promise<MarketStateQuoteSnapshot>;
|
|
165
|
+
export declare function fetchSparklineSnapshot(symbol: string): Promise<MarketSparklineSnapshot>;
|
|
107
166
|
export declare function searchInstrumentCandidates(query: string): Promise<{
|
|
108
167
|
query: string;
|
|
109
168
|
candidates: Awaited<ReturnType<typeof searchYahooInstruments>>;
|
|
@@ -121,6 +180,7 @@ export declare function getInstrumentQuoteSnapshot(symbol: string): Promise<{
|
|
|
121
180
|
low: number;
|
|
122
181
|
week52High: number;
|
|
123
182
|
week52Low: number;
|
|
183
|
+
marketCap: number;
|
|
124
184
|
fetchedAt: string;
|
|
125
185
|
dataAsOf?: string;
|
|
126
186
|
marketState?: "PRE" | "REGULAR" | "POST" | "CLOSED";
|
|
@@ -135,4 +195,56 @@ export declare function getInstrumentQuoteSnapshot(symbol: string): Promise<{
|
|
|
135
195
|
status: "unavailable";
|
|
136
196
|
reason: string;
|
|
137
197
|
}>;
|
|
198
|
+
export type InstrumentOverviewSnapshot = {
|
|
199
|
+
symbol: string;
|
|
200
|
+
status: "ok";
|
|
201
|
+
name: string;
|
|
202
|
+
description: string;
|
|
203
|
+
exchange: string;
|
|
204
|
+
sector: string;
|
|
205
|
+
industry: string;
|
|
206
|
+
marketCap: number;
|
|
207
|
+
pe: number | null;
|
|
208
|
+
forwardPe: number | null;
|
|
209
|
+
eps: number | null;
|
|
210
|
+
dividendYield: number | null;
|
|
211
|
+
beta: number | null;
|
|
212
|
+
avgVolume: number;
|
|
213
|
+
profitMargin: number | null;
|
|
214
|
+
revenueGrowth: number | null;
|
|
215
|
+
week52High: number;
|
|
216
|
+
week52Low: number;
|
|
217
|
+
stale: boolean;
|
|
218
|
+
} | {
|
|
219
|
+
symbol: string;
|
|
220
|
+
status: "unavailable";
|
|
221
|
+
reason: string;
|
|
222
|
+
};
|
|
223
|
+
export declare function getInstrumentOverviewSnapshot(symbol: string): Promise<InstrumentOverviewSnapshot>;
|
|
224
|
+
export declare function resetInstrumentOverviewMemoForTests(): void;
|
|
225
|
+
export declare function fetchQuoteSnapshot(symbol: string): Promise<{
|
|
226
|
+
status: "ok";
|
|
227
|
+
name?: string;
|
|
228
|
+
price: number;
|
|
229
|
+
change: number;
|
|
230
|
+
changePercent: number;
|
|
231
|
+
volume: number;
|
|
232
|
+
high: number;
|
|
233
|
+
low: number;
|
|
234
|
+
week52High: number;
|
|
235
|
+
week52Low: number;
|
|
236
|
+
marketCap: number;
|
|
237
|
+
fetchedAt: string;
|
|
238
|
+
dataAsOf?: string;
|
|
239
|
+
marketState?: "PRE" | "REGULAR" | "POST" | "CLOSED";
|
|
240
|
+
extendedPrice?: number;
|
|
241
|
+
extendedChange?: number;
|
|
242
|
+
extendedChangePercent?: number;
|
|
243
|
+
extendedAsOf?: string;
|
|
244
|
+
stale: boolean;
|
|
245
|
+
currency: string | null;
|
|
246
|
+
} | {
|
|
247
|
+
status: "unavailable";
|
|
248
|
+
reason: string;
|
|
249
|
+
}>;
|
|
138
250
|
export {};
|