Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions docs/provider-e2e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Live 双 Provider E2E

该检查只验证真实 Provider,不使用 mock。它分别创建 Longbridge 和 Massive 适配器,经过 `ProviderRouter` 执行共同能力 `market.quote`、`market.kline`、`company.profile`,并检查规范化结果和 `ProviderProvenance` 的 `providerId`、`fetchedAt`、`stale` 等字段。

## 前置条件

1. 安装 Longbridge CLI,并完成登录:`longbridge auth login`。
2. 设置 Massive API key。开发环境可使用 `MASSIVE_API_KEY`;兼容旧名称 `POLYGON_API_KEY`。
3. 使用美股标的,默认是 `AAPL.US`。可通过 `FINAGENT_PROVIDER_E2E_SYMBOL` 覆盖。

密钥只通过进程环境变量传给适配器,不会写入报告或日志。Massive 返回的数据可能带有延迟/日终属性,报告会保留其 `delayed` 来源标记。

## 运行

默认命令不会联网,也不会执行真实请求:

```sh
bun run test:provider-e2e
```

显式开启真实检查:

```sh
# PowerShell
$env:FINAGENT_PROVIDER_E2E = "1"
$env:MASSIVE_API_KEY = "你的密钥"
bun run test:provider-e2e
```

可选地将脱敏 JSON 报告写入文件:

```powershell
$env:FINAGENT_PROVIDER_E2E_OUTPUT = "artifacts/provider-e2e.json"
bun run test:provider-e2e
```

没有 Longbridge 登录、没有 Massive key、Provider 返回错误或规范化/来源校验失败时,命令以非零状态结束;不会把 `SKIPPED` 当作通过。普通 CI 只应运行 `bun run test:provider-e2e:guard`,真实 E2E 需要在具备两套凭据的受控环境中手动运行。

## 验收记录

提交真实验收结果时,应附上命令输出或脱敏 JSON,并记录 Bun 版本、操作系统/架构、执行时间和实际 Provider 状态。不要提交 API key、Longbridge 账户标识或供应商原始响应。
61 changes: 61 additions & 0 deletions docs/provider-e2e.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# 真实双 Provider E2E

该检查只访问真实 Provider,不使用 mock。它分别通过 `ProviderRouter` 调用 Longbridge 和 Massive 的共同能力:`market.quote`、`market.kline`、`company.profile`,并校验规范化结果及 `ProviderProvenance`。

## 前置条件

1. 安装 Longbridge CLI。Windows 可执行:

```powershell
winget install --id Longbridge.LongbridgeTerminal --location "D:\Tools\Longbridge"
```

2. 打开新的 PowerShell 窗口,完成 Longbridge 登录:

```powershell
longbridge auth login
longbridge auth status --format json
```

返回的 `token.status` 必须是 `valid`。如果当前窗口仍找不到命令,可临时执行:

```powershell
$env:Path = "D:\Tools\Longbridge;$env:Path"
```

3. 在 [Massive](https://massive.com/) 控制台创建 API Key,并设置环境变量:

```powershell
$env:MASSIVE_API_KEY = "你的 Massive API Key"
```

兼容旧变量名 `POLYGON_API_KEY`。密钥只存在于当前进程环境,不会写入报告。

## 运行

默认不会联网:

```powershell
bun run test:provider-e2e
```

显式开启真实 E2E:

```powershell
$env:FINAGENT_PROVIDER_E2E = "1"
$env:FINAGENT_PROVIDER_E2E_SYMBOL = "AAPL.US"
bun run test:provider-e2e
```

可选保存脱敏 JSON:

```powershell
$env:FINAGENT_PROVIDER_E2E_OUTPUT = "artifacts/provider-e2e.json"
bun run test:provider-e2e
```

缺少任一凭据、Longbridge 未登录、Provider 返回错误,或规范化/来源校验失败时,命令以非零状态结束;不会将 `SKIPPED` 当作通过。普通 CI 使用 `bun run test:provider-e2e:guard`,真实 E2E 需要在具备两套凭据的受控环境中手动运行。

## 验收记录

提交真实验收结果时,应附上脱敏 JSON 或命令输出,并记录 Bun 版本、操作系统/架构、执行时间和两个 Provider 的实际状态。不得提交 API Key、Longbridge 账户标识或供应商原始响应。
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
"test:ui": "bun test packages/ui",
"test:e2e": "cd apps/electron && bun run test:e2e",
"test:e2e:visible": "cd apps/electron && FINAGENT_E2E_VISIBLE=1 bun run test:e2e",
"test:provider-e2e": "bun run scripts/provider-e2e.ts",
"test:provider-e2e:guard": "bun test scripts/provider-e2e.test.ts",
"test:package-smoke": "cd apps/electron && bun run test:package-smoke",
"test:release": "bun run release:check",
"release:check": "bun run scripts/release-check.mjs",
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,30 @@ export interface Kline {
volume: number;
}

/** Provider-neutral query options for historical OHLCV bars. */
export interface KlineQueryOptions {
symbol: string;
period?: '1m' | '5m' | '15m' | '1h' | '1d' | '1w';
start?: number;
end?: number;
limit?: number;
}

/** Provider-neutral query options for finance-calendar events. */
export interface CalendarEventsQueryOptions {
eventType: 'financial' | 'report' | 'dividend' | 'ipo' | 'macrodata' | 'closed';
symbols?: string[];
start?: string;
end?: string;
count?: number;
}

/** Provider-neutral query options for account cash-flow records. */
export interface CashFlowQueryOptions {
start?: string;
end?: string;
}

export interface IntradayData {
symbol: string;
timestamp: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/en-US/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const connections = {
reconnect: 'Reconnect',
installSetup: 'Install / Setup',
byokNote:
'Your own key governs usage. Free tiers may return end-of-day data (5 calls/min) and require attribution ("Powered by Polygon.io").',
'Your own key governs usage. Massive may return delayed or end-of-day data and may require attribution ("Powered by Polygon.io").',
dismissError: 'Dismiss error',
dismiss: 'Dismiss',
portfolioReady: 'Portfolio ✓',
Expand Down
2 changes: 1 addition & 1 deletion packages/i18n/src/locales/zh-CN/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const connections = {
reconnect: '重新连接',
installSetup: '安装 / 设置',
byokNote:
'你自己的密钥决定使用情况。免费套餐可能返回当日结束后的数据(每分钟 5 次调用)并要求注明来源(“Powered by Polygon.io”)。',
'你自己的密钥决定使用情况。Massive 可能返回延迟或日终数据,并可能要求注明来源(“Powered by Polygon.io”)。',
dismissError: '关闭错误',
dismiss: '关闭',
portfolioReady: '投资组合 ✓',
Expand Down
22 changes: 10 additions & 12 deletions packages/shared/src/agent/market-data-service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import type {
AccountAssets,
CalendarEventsQueryOptions,
CalcIndex,
CashFlowQueryOptions,
CashFlowRecord,
Holding,
IntradayData,
Kline,
KlineQueryOptions,
MarketStatus,
NewsItem,
PortfolioSnapshot,
Quote,
StaticInfo,
} from '@finagent/core';
import type {
CalendarEvent,
CapitalFlow,
Depth,
Expand All @@ -21,7 +22,7 @@ import type {
InstitutionRating,
MarketTemperature,
TradeTick,
} from '@finagent/longbridge-tools';
} from '@finagent/core';
import {
getAccountPositions,
getAssets,
Expand All @@ -44,9 +45,6 @@ import {
getQuote,
getStaticInfo,
getTrades,
type GetCalendarEventsOptions,
type GetCashFlowOptions,
type GetKlineOptions,
type LongBridgeStatus,
} from '@finagent/longbridge-tools';

Expand All @@ -62,7 +60,7 @@ export interface MarketDataServiceOptions {

export interface MarketDataFetchers {
getQuote: (symbol: string) => Promise<Quote>;
getKline: (options: GetKlineOptions) => Promise<Kline[]>;
getKline: (options: KlineQueryOptions) => Promise<Kline[]>;
getIntraday: (symbol: string) => Promise<IntradayData[]>;
getPortfolio: () => Promise<PortfolioSnapshot>;
getLongBridgeStatus: () => Promise<LongBridgeStatus>;
Expand All @@ -82,10 +80,10 @@ export interface MarketDataFetchers {
getInstitutionRating: (symbol: string) => Promise<InstitutionRating>;
getDividends: (symbol: string) => Promise<DividendRecord[]>;
getEpsForecasts: (symbol: string) => Promise<EpsForecast[]>;
getCalendarEvents: (options: GetCalendarEventsOptions) => Promise<CalendarEvent[]>;
getCalendarEvents: (options: CalendarEventsQueryOptions) => Promise<CalendarEvent[]>;
getAccountPositions: () => Promise<Holding[]>;
getAssets: (currency?: string) => Promise<AccountAssets[]>;
getCashFlow: (options?: GetCashFlowOptions) => Promise<CashFlowRecord[]>;
getCashFlow: (options?: CashFlowQueryOptions) => Promise<CashFlowRecord[]>;
}

interface CacheEntry<T> {
Expand Down Expand Up @@ -142,7 +140,7 @@ export class MarketDataService {
return this.cached(`quote:${symbol}`, this.quoteTTL, () => this.fetchers.getQuote(symbol));
}

getKline(options: GetKlineOptions) {
getKline(options: KlineQueryOptions) {
const key = `kline:${options.symbol}:${options.period ?? '1d'}:${options.limit ?? 100}`;
return this.cached(key, this.klineTTL, () => this.fetchers.getKline(options));
}
Expand Down Expand Up @@ -222,7 +220,7 @@ export class MarketDataService {
);
}

getCalendarEvents(options: GetCalendarEventsOptions) {
getCalendarEvents(options: CalendarEventsQueryOptions) {
const key = `calendar:${options.eventType}:${(options.symbols ?? []).join(',')}:${options.start ?? ''}:${options.end ?? ''}:${options.count ?? 100}`;
return this.cached(key, this.referenceTTL, () => this.fetchers.getCalendarEvents(options));
}
Expand All @@ -237,7 +235,7 @@ export class MarketDataService {
);
}

getCashFlow(options: GetCashFlowOptions = {}) {
getCashFlow(options: CashFlowQueryOptions = {}) {
const key = `cash-flow:${options.start ?? ''}:${options.end ?? ''}`;
return this.cached(key, this.portfolioTTL, () => this.fetchers.getCashFlow(options));
}
Expand Down
57 changes: 51 additions & 6 deletions packages/shared/src/capabilities/fetchers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type {
AccountAssets,
CalendarEventsQueryOptions,
CalcIndex,
CashFlowQueryOptions,
CalendarEvent,
CapitalFlow,
CashFlowRecord,
Expand All @@ -12,6 +14,7 @@ import type {
InstitutionRating,
IntradayData,
Kline,
KlineQueryOptions,
MarketStatus,
MarketTemperature,
NewsItem,
Expand Down Expand Up @@ -41,10 +44,8 @@ import {
getQuote,
getStaticInfo,
getTrades,
type GetCalendarEventsOptions,
type GetCashFlowOptions,
type GetKlineOptions,
} from '@finagent/longbridge-tools';
import type { CapabilityProvenance, ProviderResult } from '@finagent/core';

/**
* Provider fetchers consumed by the capability manifests. Production uses the
Expand All @@ -54,7 +55,7 @@ import {
*/
export interface CapabilityFetchers {
getQuote: (symbol: string) => Promise<Quote>;
getKline: (options: GetKlineOptions) => Promise<Kline[]>;
getKline: (options: KlineQueryOptions) => Promise<Kline[]>;
getIntraday: (symbol: string) => Promise<IntradayData[]>;
getMarketStatus: () => Promise<MarketStatus[]>;
getStaticInfo: (symbol: string) => Promise<StaticInfo>;
Expand All @@ -73,10 +74,54 @@ export interface CapabilityFetchers {
getInstitutionRating: (symbol: string) => Promise<InstitutionRating>;
getDividends: (symbol: string) => Promise<DividendRecord[]>;
getEpsForecasts: (symbol: string) => Promise<EpsForecast[]>;
getCalendarEvents: (options: GetCalendarEventsOptions) => Promise<CalendarEvent[]>;
getCalendarEvents: (options: CalendarEventsQueryOptions) => Promise<CalendarEvent[]>;
getAccountPositions: () => Promise<Holding[]>;
getAssets: (currency?: string) => Promise<AccountAssets[]>;
getCashFlow: (options?: GetCashFlowOptions) => Promise<CashFlowRecord[]>;
getCashFlow: (options?: CashFlowQueryOptions) => Promise<CashFlowRecord[]>;
/** Optional structured gateway entry point used to preserve provenance. */
execute?: <T>(capabilityId: string, input: unknown, signal?: AbortSignal) => Promise<ProviderResult<T>>;
}

export interface ResolvedCapabilityFetch<T> {
data: T;
provenance: CapabilityProvenance;
}

/** Resolve through the provider gateway when configured, preserving its provenance. */
export async function resolveCapabilityFetch<T>(
fetchers: CapabilityFetchers,
capabilityId: string,
input: unknown,
fallback: () => Promise<T>,
now: () => number,
marketTime?: number,
signal?: AbortSignal
): Promise<ResolvedCapabilityFetch<T>> {
if (fetchers.execute) {
const result = await fetchers.execute<T>(capabilityId, input, signal);
if (!result.ok) {
const error = Object.assign(new Error(result.error.message), {
code: result.error.code,
retryable: result.error.retryable,
});
throw error;
}
return {
data: result.data,
provenance: {
provider: result.provenance.providerId,
providerId: result.provenance.providerId,
fetchedAt: result.provenance.fetchedAt,
marketTime: result.provenance.marketTime ?? marketTime,
delayed: result.provenance.delayed,
stale: result.provenance.stale,
},
};
}
return {
data: await fallback(),
provenance: { provider: 'longbridge', fetchedAt: now(), marketTime, stale: false },
};
}

export const defaultCapabilityFetchers: CapabilityFetchers = {
Expand Down
Loading