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.
Files changed (75) hide show
  1. package/README.md +48 -180
  2. package/dist/cli-main.js +9 -8
  3. package/dist/config.d.ts +4 -0
  4. package/dist/config.js +1 -0
  5. package/dist/doctor/cli-command.js +7 -4
  6. package/dist/doctor/report.js +13 -0
  7. package/dist/gui/server/chat-event-adapter.js +145 -20
  8. package/dist/gui/server/history-snapshot-store.d.ts +8 -0
  9. package/dist/gui/server/history-snapshot-store.js +43 -0
  10. package/dist/gui/server/http-routes.d.ts +2 -0
  11. package/dist/gui/server/http-routes.js +32 -7
  12. package/dist/gui/server/live-chat-event-adapter.d.ts +2 -0
  13. package/dist/gui/server/live-chat-event-adapter.js +33 -15
  14. package/dist/gui/server/market-indices-api.d.ts +21 -0
  15. package/dist/gui/server/market-indices-api.js +26 -0
  16. package/dist/gui/server/market-indices-snapshot-store.d.ts +12 -0
  17. package/dist/gui/server/market-indices-snapshot-store.js +56 -0
  18. package/dist/gui/server/market-state-api.d.ts +112 -0
  19. package/dist/gui/server/market-state-api.js +246 -4
  20. package/dist/gui/server/model-setup.d.ts +2 -11
  21. package/dist/gui/server/model-setup.js +10 -6
  22. package/dist/gui/server/server.js +12 -9
  23. package/dist/gui/server/tool-metadata.d.ts +14 -14
  24. package/dist/gui/server/ws-hub.js +1 -1
  25. package/dist/infra/cache.d.ts +4 -0
  26. package/dist/infra/cache.js +4 -0
  27. package/dist/infra/lse-byte-budget.d.ts +10 -0
  28. package/dist/infra/lse-byte-budget.js +47 -0
  29. package/dist/infra/rate-limiter.js +1 -0
  30. package/dist/onboarding/providers.d.ts +16 -1
  31. package/dist/onboarding/providers.js +20 -0
  32. package/dist/onboarding/validate-model-key.js +33 -1
  33. package/dist/onboarding/validation.js +8 -0
  34. package/dist/pi/opencandle-extension.d.ts +2 -1
  35. package/dist/pi/opencandle-extension.js +2 -2
  36. package/dist/pi/session.d.ts +2 -3
  37. package/dist/pi/session.js +7 -5
  38. package/dist/pi/setup.d.ts +3 -3
  39. package/dist/pi/setup.js +51 -59
  40. package/dist/prompts/workflow-prompts.js +16 -14
  41. package/dist/providers/index.d.ts +1 -0
  42. package/dist/providers/index.js +1 -0
  43. package/dist/providers/lse.d.ts +35 -0
  44. package/dist/providers/lse.js +284 -0
  45. package/dist/providers/polymarket.js +22 -5
  46. package/dist/providers/wrap-provider.js +1 -0
  47. package/dist/providers/yahoo-finance.js +1 -0
  48. package/dist/routing/route-manifest.js +1 -0
  49. package/dist/routing/router.js +8 -1
  50. package/dist/runtime/evidence.d.ts +1 -0
  51. package/dist/runtime/evidence.js +1 -1
  52. package/dist/runtime/session-coordinator.d.ts +2 -2
  53. package/dist/runtime/session-coordinator.js +2 -2
  54. package/dist/tools/fundamentals/dcf.js +37 -8
  55. package/dist/tools/fundamentals/financials.js +88 -10
  56. package/dist/tools/index.d.ts +10 -5
  57. package/dist/tools/index.js +3 -0
  58. package/dist/tools/market/price-comparison.d.ts +30 -0
  59. package/dist/tools/market/price-comparison.js +202 -0
  60. package/dist/tools/market/stock-history.d.ts +18 -3
  61. package/dist/tools/market/stock-history.js +132 -17
  62. package/dist/tools/portfolio/correlation.d.ts +1 -1
  63. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  64. package/dist/types/market.d.ts +2 -0
  65. package/gui/web/dist/assets/CatalogOverlay-Cy8Fq0fn.js +1 -0
  66. package/gui/web/dist/assets/allocation-donut-DgBRIKCk.js +52 -0
  67. package/gui/web/dist/assets/index-DIlFyIOO.js +66 -0
  68. package/gui/web/dist/assets/index-DS4jESOB.css +2 -0
  69. package/gui/web/dist/assets/market-chart-DammaCjH.js +1 -0
  70. package/gui/web/dist/assets/utils-CnADgYgh.js +1 -0
  71. package/gui/web/dist/index.html +3 -2
  72. package/package.json +15 -17
  73. package/gui/web/dist/assets/CatalogOverlay-CAc7e3Pf.js +0 -1
  74. package/gui/web/dist/assets/index-BOEKd9wT.css +0 -2
  75. package/gui/web/dist/assets/index-C-H05cQ2.js +0 -65
@@ -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<"6mo" | "1y" | "2y">[]>>;
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<{
@@ -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<"6mo" | "1d" | "ytd" | "5d" | "1mo" | "3mo" | "1y" | "2y" | "5y" | "10y" | "max">[]>>;
6
- interval: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"1d" | "1mo" | "1wk" | "1m" | "5m" | "15m" | "1h">[]>>;
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 declare const stockHistoryTool: AgentTool<typeof params, OHLCV[]>;
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 apiKey = getConfig().alphaVantageApiKey;
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 { content: [{ type: "text", text }], details: bars };
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<"6mo" | "1y" | "2y">[]>>;
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<"6mo" | "1y" | "2y">[]>>;
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;
@@ -24,6 +24,8 @@ export interface StockQuote {
24
24
  }
25
25
  export interface OHLCV {
26
26
  date: string;
27
+ /** UTC epoch seconds; present when the provider supplies per-bar timestamps. */
28
+ timestamp?: number;
27
29
  open: number;
28
30
  high: number;
29
31
  low: number;