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
package/dist/tools/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { fearGreedTool } from "./macro/fear-greed.js";
|
|
|
10
10
|
export { fredDataTool } from "./macro/fred-data.js";
|
|
11
11
|
export { cryptoHistoryTool } from "./market/crypto-history.js";
|
|
12
12
|
export { cryptoPriceTool } from "./market/crypto-price.js";
|
|
13
|
+
export { priceComparisonTool } from "./market/price-comparison.js";
|
|
13
14
|
export { screenStocksTool } from "./market/screen-stocks.js";
|
|
14
15
|
export { searchTickerTool } from "./market/search-ticker.js";
|
|
15
16
|
export { stockHistoryTool } from "./market/stock-history.js";
|
|
@@ -65,7 +66,7 @@ export declare function getAllTools(options?: {
|
|
|
65
66
|
credentialRequired: unknown;
|
|
66
67
|
} | null> | import("@earendil-works/pi-agent-core").AgentTool<import("@sinclair/typebox").TObject<{
|
|
67
68
|
symbol: import("@sinclair/typebox").TString;
|
|
68
|
-
period: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"
|
|
69
|
+
period: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"1y" | "2y" | "6mo">[]>>;
|
|
69
70
|
}>, import("../types/portfolio.js").RiskMetrics | null> | import("@earendil-works/pi-agent-core").AgentTool<import("@sinclair/typebox").TObject<{
|
|
70
71
|
id: import("@sinclair/typebox").TString;
|
|
71
72
|
days: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
|
|
@@ -74,6 +75,14 @@ export declare function getAllTools(options?: {
|
|
|
74
75
|
}>, (import("../types/market.js").CryptoPrice & {
|
|
75
76
|
freshness: import("../infra/freshness.js").FreshnessStamp;
|
|
76
77
|
}) | null> | import("@earendil-works/pi-agent-core").AgentTool<import("@sinclair/typebox").TObject<{
|
|
78
|
+
symbol: import("@sinclair/typebox").TString;
|
|
79
|
+
range: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"10y" | "1d" | "1mo" | "1y" | "2y" | "3mo" | "5d" | "5y" | "6mo" | "max" | "ytd">[]>>;
|
|
80
|
+
interval: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"15m" | "1d" | "1h" | "1m" | "1mo" | "1wk" | "5m">[]>>;
|
|
81
|
+
}>, import("../types/market.js").OHLCV[] | import("./market/stock-history.js").StaleStockHistoryDetails> | import("@earendil-works/pi-agent-core").AgentTool<import("@sinclair/typebox").TObject<{
|
|
82
|
+
symbols: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
83
|
+
range: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"10y" | "1d" | "1mo" | "1y" | "2y" | "3mo" | "5d" | "5y" | "6mo" | "max" | "ytd">[]>;
|
|
84
|
+
interval: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"15m" | "1d" | "1h" | "1m" | "1mo" | "1wk" | "5m">[]>>;
|
|
85
|
+
}>, import("./market/price-comparison.js").PriceComparisonDetails> | import("@earendil-works/pi-agent-core").AgentTool<import("@sinclair/typebox").TObject<{
|
|
77
86
|
market: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
78
87
|
columns: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
|
|
79
88
|
filter: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
@@ -90,10 +99,6 @@ export declare function getAllTools(options?: {
|
|
|
90
99
|
query: import("@sinclair/typebox").TString;
|
|
91
100
|
}>, any> | import("@earendil-works/pi-agent-core").AgentTool<import("@sinclair/typebox").TObject<{
|
|
92
101
|
symbol: import("@sinclair/typebox").TString;
|
|
93
|
-
range: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"6mo" | "1d" | "ytd" | "5d" | "1mo" | "3mo" | "1y" | "2y" | "5y" | "10y" | "max">[]>>;
|
|
94
|
-
interval: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"1d" | "1mo" | "1wk" | "1m" | "5m" | "15m" | "1h">[]>>;
|
|
95
|
-
}>, import("../types/market.js").OHLCV[]> | import("@earendil-works/pi-agent-core").AgentTool<import("@sinclair/typebox").TObject<{
|
|
96
|
-
symbol: import("@sinclair/typebox").TString;
|
|
97
102
|
}>, (import("../types/market.js").StockQuote & {
|
|
98
103
|
freshness: import("../infra/freshness.js").FreshnessStamp;
|
|
99
104
|
}) | null> | import("@earendil-works/pi-agent-core").AgentTool<import("@sinclair/typebox").TObject<{
|
package/dist/tools/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { fearGreedTool } from "./macro/fear-greed.js";
|
|
|
9
9
|
import { fredDataTool } from "./macro/fred-data.js";
|
|
10
10
|
import { cryptoHistoryTool } from "./market/crypto-history.js";
|
|
11
11
|
import { cryptoPriceTool } from "./market/crypto-price.js";
|
|
12
|
+
import { priceComparisonTool } from "./market/price-comparison.js";
|
|
12
13
|
import { screenStocksTool } from "./market/screen-stocks.js";
|
|
13
14
|
import { searchTickerTool } from "./market/search-ticker.js";
|
|
14
15
|
import { stockHistoryTool } from "./market/stock-history.js";
|
|
@@ -41,6 +42,7 @@ export { fearGreedTool } from "./macro/fear-greed.js";
|
|
|
41
42
|
export { fredDataTool } from "./macro/fred-data.js";
|
|
42
43
|
export { cryptoHistoryTool } from "./market/crypto-history.js";
|
|
43
44
|
export { cryptoPriceTool } from "./market/crypto-price.js";
|
|
45
|
+
export { priceComparisonTool } from "./market/price-comparison.js";
|
|
44
46
|
export { screenStocksTool } from "./market/screen-stocks.js";
|
|
45
47
|
export { searchTickerTool } from "./market/search-ticker.js";
|
|
46
48
|
export { stockHistoryTool } from "./market/stock-history.js";
|
|
@@ -67,6 +69,7 @@ export function getAllTools(options = {}) {
|
|
|
67
69
|
searchTickerTool,
|
|
68
70
|
stockQuoteTool,
|
|
69
71
|
stockHistoryTool,
|
|
72
|
+
priceComparisonTool,
|
|
70
73
|
screenStocksTool,
|
|
71
74
|
cryptoPriceTool,
|
|
72
75
|
cryptoHistoryTool,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { type FreshnessStamp } from "../../infra/freshness.js";
|
|
3
|
+
import type { OHLCV } from "../../types/market.js";
|
|
4
|
+
import { HISTORY_INTERVALS, HISTORY_RANGES } from "./stock-history.js";
|
|
5
|
+
type HistoryRange = (typeof HISTORY_RANGES)[number];
|
|
6
|
+
type HistoryInterval = (typeof HISTORY_INTERVALS)[number];
|
|
7
|
+
export interface PriceComparisonSeries {
|
|
8
|
+
symbol: string;
|
|
9
|
+
bars: OHLCV[];
|
|
10
|
+
indexed: number[];
|
|
11
|
+
provider?: string;
|
|
12
|
+
providerTimestamp: string;
|
|
13
|
+
cached: boolean;
|
|
14
|
+
stale: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface PriceComparisonDetails {
|
|
17
|
+
range: HistoryRange;
|
|
18
|
+
interval: HistoryInterval;
|
|
19
|
+
baseDate: string;
|
|
20
|
+
series: PriceComparisonSeries[];
|
|
21
|
+
unavailableSymbols: string[];
|
|
22
|
+
freshness: FreshnessStamp;
|
|
23
|
+
}
|
|
24
|
+
declare const params: import("@sinclair/typebox").TObject<{
|
|
25
|
+
symbols: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
26
|
+
range: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"10y" | "1d" | "1mo" | "1y" | "2y" | "3mo" | "5d" | "5y" | "6mo" | "max" | "ytd">[]>;
|
|
27
|
+
interval: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"15m" | "1d" | "1h" | "1m" | "1mo" | "1wk" | "5m">[]>>;
|
|
28
|
+
}>;
|
|
29
|
+
export declare const priceComparisonTool: AgentTool<typeof params, PriceComparisonDetails>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { buildFreshnessStamp, formatAsOfLine } from "../../infra/freshness.js";
|
|
3
|
+
import { fetchHistoryWithFallback, HISTORY_INTERVALS, HISTORY_RANGES } from "./stock-history.js";
|
|
4
|
+
const DATE_ALIGNED_INTERVALS = new Set(["1d", "1wk", "1mo"]);
|
|
5
|
+
const params = Type.Object({
|
|
6
|
+
symbols: Type.Array(Type.String({ description: "Ticker symbol" }), {
|
|
7
|
+
minItems: 2,
|
|
8
|
+
maxItems: 6,
|
|
9
|
+
description: "Ticker symbols to compare",
|
|
10
|
+
}),
|
|
11
|
+
range: Type.Union(HISTORY_RANGES.map((range) => Type.Literal(range)), {
|
|
12
|
+
description: "History range",
|
|
13
|
+
}),
|
|
14
|
+
interval: Type.Optional(Type.Union(HISTORY_INTERVALS.map((interval) => Type.Literal(interval)), { description: "Data interval. Default: 1d" })),
|
|
15
|
+
});
|
|
16
|
+
export const priceComparisonTool = {
|
|
17
|
+
name: "get_price_comparison",
|
|
18
|
+
label: "Price Comparison",
|
|
19
|
+
description: "Get aligned indexed price history for multiple ticker symbols",
|
|
20
|
+
parameters: params,
|
|
21
|
+
async execute(_toolCallId, args) {
|
|
22
|
+
const symbols = args.symbols.map((symbol) => symbol.toUpperCase());
|
|
23
|
+
const interval = args.interval ?? "1d";
|
|
24
|
+
const fetched = await Promise.all(symbols.map(async (symbol) => ({
|
|
25
|
+
symbol,
|
|
26
|
+
result: await fetchHistoryWithFallback(symbol, args.range, interval),
|
|
27
|
+
})));
|
|
28
|
+
const available = fetched.flatMap(({ symbol, result }) => result.status === "ok" && result.data.length > 0
|
|
29
|
+
? [
|
|
30
|
+
{
|
|
31
|
+
symbol,
|
|
32
|
+
bars: result.data,
|
|
33
|
+
provider: result.provider,
|
|
34
|
+
providerTimestamp: result.timestamp,
|
|
35
|
+
cached: result.cached === true,
|
|
36
|
+
stale: result.stale === true,
|
|
37
|
+
},
|
|
38
|
+
]
|
|
39
|
+
: []);
|
|
40
|
+
let unavailableSymbols = fetched.flatMap(({ symbol, result }) => result.status === "unavailable" || result.data.length === 0 ? [symbol] : []);
|
|
41
|
+
if (available.length < 2) {
|
|
42
|
+
const freshness = buildFreshnessStamp({});
|
|
43
|
+
const unavailableText = unavailableSymbols.join(", ") || symbols.join(", ");
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: [
|
|
49
|
+
`Price comparison unavailable: fewer than 2 series.`,
|
|
50
|
+
`Unavailable symbols: ${unavailableText}`,
|
|
51
|
+
formatAsOfLine(freshness),
|
|
52
|
+
].join("\n"),
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
details: {
|
|
56
|
+
range: args.range,
|
|
57
|
+
interval,
|
|
58
|
+
baseDate: "",
|
|
59
|
+
series: [],
|
|
60
|
+
unavailableSymbols,
|
|
61
|
+
freshness,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
let survivingAvailable = available;
|
|
66
|
+
let commonKeys = intersectAlignmentKeys(survivingAvailable, interval);
|
|
67
|
+
while (survivingAvailable.length >= 2 && commonKeys.size > 0) {
|
|
68
|
+
const invalidBaselineSymbols = survivingAvailable.flatMap(({ symbol, bars }) => {
|
|
69
|
+
const baseClose = bars.find((bar) => {
|
|
70
|
+
const key = alignmentKey(bar, interval);
|
|
71
|
+
return key !== undefined && commonKeys.has(key);
|
|
72
|
+
})?.close;
|
|
73
|
+
return typeof baseClose !== "number" || !Number.isFinite(baseClose) || baseClose <= 0
|
|
74
|
+
? [symbol]
|
|
75
|
+
: [];
|
|
76
|
+
});
|
|
77
|
+
if (invalidBaselineSymbols.length === 0)
|
|
78
|
+
break;
|
|
79
|
+
unavailableSymbols = [...new Set([...unavailableSymbols, ...invalidBaselineSymbols])];
|
|
80
|
+
const invalid = new Set(invalidBaselineSymbols);
|
|
81
|
+
survivingAvailable = survivingAvailable.filter(({ symbol }) => !invalid.has(symbol));
|
|
82
|
+
commonKeys = intersectAlignmentKeys(survivingAvailable, interval);
|
|
83
|
+
}
|
|
84
|
+
if (survivingAvailable.length < 2) {
|
|
85
|
+
return unavailableComparison(args.range, interval, unavailableSymbols);
|
|
86
|
+
}
|
|
87
|
+
if (commonKeys.size === 0) {
|
|
88
|
+
unavailableSymbols = [
|
|
89
|
+
...new Set([...unavailableSymbols, ...survivingAvailable.map(({ symbol }) => symbol)]),
|
|
90
|
+
];
|
|
91
|
+
return unavailableComparison(args.range, interval, unavailableSymbols);
|
|
92
|
+
}
|
|
93
|
+
const series = survivingAvailable.map(({ symbol, bars, provider, providerTimestamp, cached, stale }) => {
|
|
94
|
+
const alignedBars = bars.filter((bar) => {
|
|
95
|
+
const key = alignmentKey(bar, interval);
|
|
96
|
+
return key !== undefined && commonKeys.has(key);
|
|
97
|
+
});
|
|
98
|
+
const baseClose = alignedBars[0].close;
|
|
99
|
+
return {
|
|
100
|
+
symbol,
|
|
101
|
+
bars: alignedBars,
|
|
102
|
+
indexed: alignedBars.map((bar) => (bar.close / baseClose) * 100),
|
|
103
|
+
provider,
|
|
104
|
+
providerTimestamp,
|
|
105
|
+
cached,
|
|
106
|
+
stale,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const baseDate = series[0]?.bars[0]?.date ?? "";
|
|
110
|
+
const latestDate = series[0]?.bars.at(-1)?.date;
|
|
111
|
+
const staleSeries = series.filter((item) => item.stale);
|
|
112
|
+
const freshness = buildFreshnessStamp({
|
|
113
|
+
asOf: latestDate,
|
|
114
|
+
cached: series.some((item) => item.cached),
|
|
115
|
+
stale: staleSeries.length > 0,
|
|
116
|
+
cachedAt: staleSeries.map((item) => item.providerTimestamp).sort()[0],
|
|
117
|
+
});
|
|
118
|
+
const rows = series.map(({ symbol, bars, indexed }) => {
|
|
119
|
+
const first = bars[0]?.close ?? 0;
|
|
120
|
+
const last = bars.at(-1)?.close ?? 0;
|
|
121
|
+
const change = (indexed.at(-1) ?? 100) - 100;
|
|
122
|
+
return `${symbol} | ${first.toFixed(2)} | ${last.toFixed(2)} | ${change.toFixed(2)}%`;
|
|
123
|
+
});
|
|
124
|
+
const alignmentLosses = survivingAvailable.flatMap(({ symbol, bars }) => {
|
|
125
|
+
const dropped = bars.length - commonKeys.size;
|
|
126
|
+
return dropped / bars.length > 0.3
|
|
127
|
+
? [`${symbol} (${dropped}/${bars.length} dates dropped)`]
|
|
128
|
+
: [];
|
|
129
|
+
});
|
|
130
|
+
const alignmentLine = alignmentLosses.length > 0 ? [`Reduced aligned window: ${alignmentLosses.join(", ")}`] : [];
|
|
131
|
+
const unavailableLine = unavailableSymbols.length > 0
|
|
132
|
+
? [`Unavailable symbols: ${unavailableSymbols.join(", ")}`]
|
|
133
|
+
: [];
|
|
134
|
+
const staleLine = staleSeries.length > 0
|
|
135
|
+
? [
|
|
136
|
+
`Stale series: ${staleSeries
|
|
137
|
+
.map((item) => `${item.symbol} (${item.provider ?? "unknown provider"})`)
|
|
138
|
+
.join(", ")}`,
|
|
139
|
+
]
|
|
140
|
+
: [];
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: "text",
|
|
145
|
+
text: [
|
|
146
|
+
...rows,
|
|
147
|
+
...alignmentLine,
|
|
148
|
+
...unavailableLine,
|
|
149
|
+
...staleLine,
|
|
150
|
+
formatAsOfLine(freshness),
|
|
151
|
+
].join("\n"),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
details: {
|
|
155
|
+
range: args.range,
|
|
156
|
+
interval,
|
|
157
|
+
baseDate,
|
|
158
|
+
series,
|
|
159
|
+
unavailableSymbols,
|
|
160
|
+
freshness,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
function intersectAlignmentKeys(series, interval) {
|
|
166
|
+
const commonKeys = new Set(series[0]?.bars.flatMap((bar) => {
|
|
167
|
+
const key = alignmentKey(bar, interval);
|
|
168
|
+
return key === undefined ? [] : [key];
|
|
169
|
+
}) ?? []);
|
|
170
|
+
for (const candidate of series.slice(1)) {
|
|
171
|
+
const candidateKeys = new Set(candidate.bars.flatMap((bar) => {
|
|
172
|
+
const key = alignmentKey(bar, interval);
|
|
173
|
+
return key === undefined ? [] : [key];
|
|
174
|
+
}));
|
|
175
|
+
for (const key of commonKeys) {
|
|
176
|
+
if (!candidateKeys.has(key))
|
|
177
|
+
commonKeys.delete(key);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return commonKeys;
|
|
181
|
+
}
|
|
182
|
+
function alignmentKey(bar, interval) {
|
|
183
|
+
if (DATE_ALIGNED_INTERVALS.has(interval))
|
|
184
|
+
return bar.date;
|
|
185
|
+
return Number.isFinite(bar.timestamp) ? bar.timestamp : undefined;
|
|
186
|
+
}
|
|
187
|
+
function unavailableComparison(range, interval, unavailableSymbols) {
|
|
188
|
+
const freshness = buildFreshnessStamp({});
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "text",
|
|
193
|
+
text: [
|
|
194
|
+
"Price comparison unavailable: fewer than 2 usable aligned series.",
|
|
195
|
+
`Unavailable symbols: ${unavailableSymbols.join(", ")}`,
|
|
196
|
+
formatAsOfLine(freshness),
|
|
197
|
+
].join("\n"),
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
details: { range, interval, baseDate: "", series: [], unavailableSymbols, freshness },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { type FreshnessStamp } from "../../infra/freshness.js";
|
|
3
|
+
import type { ProviderResult } from "../../runtime/evidence.js";
|
|
2
4
|
import type { OHLCV } from "../../types/market.js";
|
|
5
|
+
export declare const HISTORY_RANGES: readonly ["1d", "5d", "1mo", "3mo", "6mo", "ytd", "1y", "2y", "5y", "10y", "max"];
|
|
6
|
+
export declare const HISTORY_INTERVALS: readonly ["1m", "5m", "15m", "1h", "1d", "1wk", "1mo"];
|
|
7
|
+
export declare function hasIntradayHistoryFallback(interval: (typeof HISTORY_INTERVALS)[number]): boolean;
|
|
8
|
+
export declare function fetchHistoryWithFallback(symbol: string, range: (typeof HISTORY_RANGES)[number], interval: (typeof HISTORY_INTERVALS)[number]): Promise<ProviderResult<OHLCV[]>>;
|
|
9
|
+
type HistoryRange = (typeof HISTORY_RANGES)[number];
|
|
10
|
+
export declare function historyRangeToStart(range: HistoryRange, now?: Date): string | undefined;
|
|
3
11
|
declare const params: import("@sinclair/typebox").TObject<{
|
|
4
12
|
symbol: import("@sinclair/typebox").TString;
|
|
5
|
-
range: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"
|
|
6
|
-
interval: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"
|
|
13
|
+
range: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"10y" | "1d" | "1mo" | "1y" | "2y" | "3mo" | "5d" | "5y" | "6mo" | "max" | "ytd">[]>>;
|
|
14
|
+
interval: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"15m" | "1d" | "1h" | "1m" | "1mo" | "1wk" | "5m">[]>>;
|
|
7
15
|
}>;
|
|
8
|
-
export
|
|
16
|
+
export interface StaleStockHistoryDetails {
|
|
17
|
+
bars: OHLCV[];
|
|
18
|
+
provider?: string;
|
|
19
|
+
providerTimestamp: string;
|
|
20
|
+
stale: true;
|
|
21
|
+
freshness: FreshnessStamp;
|
|
22
|
+
}
|
|
23
|
+
export declare const stockHistoryTool: AgentTool<typeof params, OHLCV[] | StaleStockHistoryDetails>;
|
|
9
24
|
export {};
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { getConfig } from "../../config.js";
|
|
3
|
+
import { buildFreshnessStamp, formatAsOfLine } from "../../infra/freshness.js";
|
|
4
|
+
import { isOverSoftThreshold } from "../../infra/lse-byte-budget.js";
|
|
3
5
|
import { getDailyHistory } from "../../providers/alpha-vantage.js";
|
|
6
|
+
import { getLseCandles, toLseTimeframe } from "../../providers/lse.js";
|
|
7
|
+
import { ProviderCredentialError } from "../../providers/provider-credential-error.js";
|
|
4
8
|
import { withFallback } from "../../providers/with-fallback.js";
|
|
5
9
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
6
10
|
import { getHistory } from "../../providers/yahoo-finance.js";
|
|
7
11
|
const DAILY_INTERVALS = new Set(["1d", "1wk", "1mo"]);
|
|
8
|
-
const HISTORY_RANGES = [
|
|
12
|
+
export const HISTORY_RANGES = [
|
|
9
13
|
"1d",
|
|
10
14
|
"5d",
|
|
11
15
|
"1mo",
|
|
@@ -18,7 +22,112 @@ const HISTORY_RANGES = [
|
|
|
18
22
|
"10y",
|
|
19
23
|
"max",
|
|
20
24
|
];
|
|
21
|
-
const HISTORY_INTERVALS = ["1m", "5m", "15m", "1h", "1d", "1wk", "1mo"];
|
|
25
|
+
export const HISTORY_INTERVALS = ["1m", "5m", "15m", "1h", "1d", "1wk", "1mo"];
|
|
26
|
+
function lseFallbackEligible(interval) {
|
|
27
|
+
return (toLseTimeframe(interval) !== undefined && !!getConfig().lseApiKey && !isOverSoftThreshold());
|
|
28
|
+
}
|
|
29
|
+
export function hasIntradayHistoryFallback(interval) {
|
|
30
|
+
return !DAILY_INTERVALS.has(interval) && lseFallbackEligible(interval);
|
|
31
|
+
}
|
|
32
|
+
export async function fetchHistoryWithFallback(symbol, range, interval) {
|
|
33
|
+
const apiKey = getConfig().alphaVantageApiKey;
|
|
34
|
+
const lseTimeframe = toLseTimeframe(interval);
|
|
35
|
+
const lseEligible = lseTimeframe !== undefined && lseFallbackEligible(interval);
|
|
36
|
+
const getLseHistory = async (timeframe) => {
|
|
37
|
+
let rows;
|
|
38
|
+
try {
|
|
39
|
+
const start = historyRangeToStart(range);
|
|
40
|
+
rows = await getLseCandles(symbol, timeframe, {
|
|
41
|
+
...(start === undefined ? {} : { start }),
|
|
42
|
+
order: "asc",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof ProviderCredentialError && error.provider === "lse") {
|
|
47
|
+
throw new Error(`LSE credential ${error.reason}`);
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return rows.map((row) => {
|
|
52
|
+
// Verified UTC from a live 2026-07-14 1h capture: naive 08:00–23:00 bars
|
|
53
|
+
// match the 04:00–19:00 ET extended session. Treat naive values as UTC.
|
|
54
|
+
const utcTimestamp = /(?:Z|[+-]\d{2}:\d{2})$/i.test(row.ts) ? row.ts : `${row.ts}Z`;
|
|
55
|
+
const timestamp = Date.parse(utcTimestamp) / 1_000;
|
|
56
|
+
return {
|
|
57
|
+
date: row.ts.slice(0, 10),
|
|
58
|
+
...(Number.isFinite(timestamp) ? { timestamp } : {}),
|
|
59
|
+
open: row.open,
|
|
60
|
+
high: row.high,
|
|
61
|
+
low: row.low,
|
|
62
|
+
close: row.close,
|
|
63
|
+
volume: row.volume,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
if (DAILY_INTERVALS.has(interval)) {
|
|
68
|
+
const yahooEntry = {
|
|
69
|
+
provider: "yahoo",
|
|
70
|
+
fn: () => getHistory(symbol, range, interval),
|
|
71
|
+
};
|
|
72
|
+
const entries = [yahooEntry];
|
|
73
|
+
if (apiKey) {
|
|
74
|
+
entries.push({
|
|
75
|
+
provider: "alphavantage",
|
|
76
|
+
fn: () => getDailyHistory(symbol, apiKey, range),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (lseEligible && lseTimeframe !== undefined) {
|
|
80
|
+
entries.push({ provider: "lse", fn: () => getLseHistory(lseTimeframe) });
|
|
81
|
+
}
|
|
82
|
+
return entries.length === 1
|
|
83
|
+
? wrapProvider(yahooEntry.provider, yahooEntry.fn)
|
|
84
|
+
: withFallback(entries);
|
|
85
|
+
}
|
|
86
|
+
if (lseEligible && lseTimeframe !== undefined) {
|
|
87
|
+
return withFallback([
|
|
88
|
+
{ provider: "yahoo", fn: () => getHistory(symbol, range, interval) },
|
|
89
|
+
{ provider: "lse", fn: () => getLseHistory(lseTimeframe) },
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
return wrapProvider("yahoo", () => getHistory(symbol, range, interval));
|
|
93
|
+
}
|
|
94
|
+
export function historyRangeToStart(range, now = new Date()) {
|
|
95
|
+
if (range === "max")
|
|
96
|
+
return undefined;
|
|
97
|
+
const start = new Date(now);
|
|
98
|
+
switch (range) {
|
|
99
|
+
case "1d":
|
|
100
|
+
start.setUTCDate(start.getUTCDate() - 1);
|
|
101
|
+
break;
|
|
102
|
+
case "5d":
|
|
103
|
+
start.setUTCDate(start.getUTCDate() - 5);
|
|
104
|
+
break;
|
|
105
|
+
case "1mo":
|
|
106
|
+
start.setUTCMonth(start.getUTCMonth() - 1);
|
|
107
|
+
break;
|
|
108
|
+
case "3mo":
|
|
109
|
+
start.setUTCMonth(start.getUTCMonth() - 3);
|
|
110
|
+
break;
|
|
111
|
+
case "6mo":
|
|
112
|
+
start.setUTCMonth(start.getUTCMonth() - 6);
|
|
113
|
+
break;
|
|
114
|
+
case "ytd":
|
|
115
|
+
return `${start.getUTCFullYear()}-01-01`;
|
|
116
|
+
case "1y":
|
|
117
|
+
start.setUTCFullYear(start.getUTCFullYear() - 1);
|
|
118
|
+
break;
|
|
119
|
+
case "2y":
|
|
120
|
+
start.setUTCFullYear(start.getUTCFullYear() - 2);
|
|
121
|
+
break;
|
|
122
|
+
case "5y":
|
|
123
|
+
start.setUTCFullYear(start.getUTCFullYear() - 5);
|
|
124
|
+
break;
|
|
125
|
+
case "10y":
|
|
126
|
+
start.setUTCFullYear(start.getUTCFullYear() - 10);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return start.toISOString().slice(0, 10);
|
|
130
|
+
}
|
|
22
131
|
const params = Type.Object({
|
|
23
132
|
symbol: Type.String({ description: "Stock ticker symbol (e.g. AAPL, MSFT)" }),
|
|
24
133
|
range: Type.Optional(Type.Union(HISTORY_RANGES.map((range) => Type.Literal(range)), {
|
|
@@ -37,21 +146,9 @@ export const stockHistoryTool = {
|
|
|
37
146
|
const symbol = args.symbol.toUpperCase();
|
|
38
147
|
const range = args.range ?? "6mo";
|
|
39
148
|
const interval = args.interval ?? "1d";
|
|
40
|
-
const
|
|
41
|
-
let result;
|
|
42
|
-
if (DAILY_INTERVALS.has(interval) && apiKey) {
|
|
43
|
-
// Daily or above — can fall back to Alpha Vantage
|
|
44
|
-
result = await withFallback([
|
|
45
|
-
{ provider: "yahoo", fn: () => getHistory(symbol, range, interval) },
|
|
46
|
-
{ provider: "alphavantage", fn: () => getDailyHistory(symbol, apiKey, range) },
|
|
47
|
-
]);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
// Intraday — no cross-provider fallback
|
|
51
|
-
result = await wrapProvider("yahoo", () => getHistory(symbol, range, interval));
|
|
52
|
-
}
|
|
149
|
+
const result = await fetchHistoryWithFallback(symbol, range, interval);
|
|
53
150
|
if (result.status === "unavailable") {
|
|
54
|
-
const intradayNote = !DAILY_INTERVALS.has(interval)
|
|
151
|
+
const intradayNote = !DAILY_INTERVALS.has(interval) && !hasIntradayHistoryFallback(interval)
|
|
55
152
|
? ` No alternate source for ${interval} data.`
|
|
56
153
|
: "";
|
|
57
154
|
return {
|
|
@@ -65,9 +162,16 @@ export const stockHistoryTool = {
|
|
|
65
162
|
};
|
|
66
163
|
}
|
|
67
164
|
const bars = result.data;
|
|
165
|
+
const freshness = buildFreshnessStamp({
|
|
166
|
+
asOf: bars.at(-1)?.timestamp ?? bars.at(-1)?.date,
|
|
167
|
+
stale: result.stale,
|
|
168
|
+
cached: result.cached,
|
|
169
|
+
cachedAt: result.stale ? result.timestamp : undefined,
|
|
170
|
+
});
|
|
68
171
|
const summary = [
|
|
69
172
|
`${symbol} — ${bars.length} bars (${range}, ${interval})`,
|
|
70
173
|
`Period: ${bars[0]?.date} to ${bars[bars.length - 1]?.date}`,
|
|
174
|
+
...(result.stale ? [`Data status: Stale cache. ${formatAsOfLine(freshness)}`] : []),
|
|
71
175
|
];
|
|
72
176
|
// Include last 10 bars as sample
|
|
73
177
|
const recent = bars.slice(-10);
|
|
@@ -75,6 +179,17 @@ export const stockHistoryTool = {
|
|
|
75
179
|
.map((b) => `${b.date} | O:${b.open.toFixed(2)} H:${b.high.toFixed(2)} L:${b.low.toFixed(2)} C:${b.close.toFixed(2)} V:${b.volume.toLocaleString()}`)
|
|
76
180
|
.join("\n");
|
|
77
181
|
const text = [...summary, "", "Recent bars:", table].join("\n");
|
|
78
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text }],
|
|
184
|
+
details: result.stale
|
|
185
|
+
? {
|
|
186
|
+
bars,
|
|
187
|
+
provider: result.provider,
|
|
188
|
+
providerTimestamp: result.timestamp,
|
|
189
|
+
stale: true,
|
|
190
|
+
freshness,
|
|
191
|
+
}
|
|
192
|
+
: bars,
|
|
193
|
+
};
|
|
79
194
|
},
|
|
80
195
|
};
|
|
@@ -4,7 +4,7 @@ export declare function computeCorrelation(returnsA: number[], returnsB: number[
|
|
|
4
4
|
export declare function alignReturnsByDate(historiesBySymbol: Map<string, OHLCV[]>, minOverlap?: number): Map<string, number[]>;
|
|
5
5
|
declare const params: import("@sinclair/typebox").TObject<{
|
|
6
6
|
symbols: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
|
|
7
|
-
period: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"
|
|
7
|
+
period: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"1y" | "2y" | "6mo">[]>>;
|
|
8
8
|
}>;
|
|
9
9
|
export declare const correlationTool: AgentTool<typeof params>;
|
|
10
10
|
export {};
|
|
@@ -2,7 +2,7 @@ import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import type { RiskMetrics } from "../../types/portfolio.js";
|
|
3
3
|
declare const params: import("@sinclair/typebox").TObject<{
|
|
4
4
|
symbol: import("@sinclair/typebox").TString;
|
|
5
|
-
period: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"
|
|
5
|
+
period: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"1y" | "2y" | "6mo">[]>>;
|
|
6
6
|
}>;
|
|
7
7
|
export declare const riskAnalysisTool: AgentTool<typeof params, RiskMetrics | null>;
|
|
8
8
|
export declare function computeRiskMetrics(symbol: string, closes: number[]): RiskMetrics;
|
package/dist/types/market.d.ts
CHANGED