diff --git a/README.md b/README.md index e920b57..20da37c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Open Alice + Open Alice

@@ -14,6 +14,10 @@ Your one-person Wall Street. Alice is an AI trading agent that gives you your ow - **Reasoning-driven** — every trading decision is based on continuous reasoning and signal mixing. - **OS-native** — Alice can interact with your operating system. Search the web through your browser, send messages via Telegram, and connect to local devices. +

+ Open Alice Preview +

+ ## Features - **Multi-provider AI** — switch between Claude Code CLI, Vercel AI SDK, and Agent SDK at runtime, no restart needed diff --git a/alice-full.png b/docs/images/alice-full.png similarity index 100% rename from alice-full.png rename to docs/images/alice-full.png diff --git a/docs/opentypebb-tutorial.md b/docs/opentypebb-tutorial.md new file mode 100644 index 0000000..aea02c7 --- /dev/null +++ b/docs/opentypebb-tutorial.md @@ -0,0 +1,314 @@ +# Running OpenTypeBB with OpenAlice + +OpenTypeBB is a TypeScript-native port of the [OpenBB Platform](https://github.com/OpenBB-finance/OpenBB) — the open-source financial data infrastructure. It ships as an internal package (`@traderalice/opentypebb`) inside OpenAlice, giving you access to equity, crypto, currency, commodity, economy, and news data without spinning up a Python sidecar or messing with `uv`. + +This tutorial walks you through getting OpenAlice up and running with OpenTypeBB as the data backend. + +--- + +## Prerequisites + +| Requirement | Version | +|-------------|---------| +| [Node.js](https://nodejs.org/) | 22+ | +| [pnpm](https://pnpm.io/) | 10+ | +| [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) | latest (installed & authenticated) | + +That's it. No Python, no `uv`, no Docker. + +--- + +## 1. Clone & Install + +```bash +git clone https://github.com/TraderAlice/OpenAlice.git +cd OpenAlice +pnpm install +pnpm build +``` + +`pnpm install` resolves the monorepo workspace — the `@traderalice/opentypebb` package under `packages/opentypebb/` is linked automatically. + +## 2. Start the Dev Server + +```bash +pnpm dev +``` + +Open [http://localhost:3002](http://localhost:3002) and start chatting. No API keys or extra config required — the default setup uses Claude Code as the AI backend with your existing login, and OpenTypeBB as the data engine via in-process SDK mode. + +> For frontend hot-reload during development, run `pnpm dev:ui` (port 5173) in a separate terminal. + +## 3. Verify OpenTypeBB Is Active + +OpenTypeBB is the **default** data backend. You don't need to configure anything — it's already on. + +Under the hood, OpenAlice reads `data/config/openbb.json`. If that file doesn't exist yet, it falls back to these defaults: + +```json +{ + "enabled": true, + "dataBackend": "sdk", + "providers": { + "equity": "yfinance", + "crypto": "yfinance", + "currency": "yfinance", + "newsCompany": "yfinance", + "newsWorld": "fmp" + }, + "providerKeys": {}, + "apiServer": { + "enabled": false, + "port": 6901 + } +} +``` + +Key settings: + +- **`dataBackend: "sdk"`** — This is the in-process mode. OpenTypeBB's `QueryExecutor` runs directly inside the Node.js process — no HTTP, no sidecar. This is the default. +- **`dataBackend: "openbb"`** — Switches to making HTTP requests to an external OpenBB Platform API server (Python). You probably don't want this. +- **`providers`** — Which data provider to use per asset class. `yfinance` works out of the box with no API key. + +## 4. Available Data Providers + +OpenTypeBB ships with 14 providers: + +| Provider | Covers | API Key? | +|----------|--------|----------| +| **yfinance** | Equity, crypto, currency, news | No | +| **fmp** | Equity fundamentals, news, discovery | Required | +| **intrinio** | Options data | Required | +| **eia** | Energy (petroleum, natural gas, electricity) | Required | +| **econdb** | Global economic data | Optional (higher rate limits) | +| **federal_reserve** | FRED, FOMC, payrolls, PCE, Michigan, etc. | Optional (higher rate limits) | +| **bls** | Bureau of Labor Statistics employment data | Optional (higher rate limits) | +| **deribit** | Crypto derivatives | No | +| **cboe** | Index data | No | +| **multpl** | S&P 500 multiples (PE, earnings yield) | No | +| **oecd** | GDP, economic indicators | No | +| **imf** | International trade, CPI, balance of payments | No | +| **ecb** | European balance of payments | No | +| **stub** | Test/placeholder | No | + +**Most things work without any API key** — `yfinance` covers equity quotes, crypto prices, forex rates, and company news. `federal_reserve`, `bls`, and `econdb` also work keyless but with stricter rate limits. Only `fmp`, `intrinio`, and `eia` strictly require a key. + +## 5. Adding API Keys (Optional) + +To unlock additional providers (FMP, EIA, Intrinio, etc.), create or edit `data/config/openbb.json`: + +```json +{ + "providerKeys": { + "fmp": "your_fmp_api_key_here", + "eia": "your_eia_api_key_here" + } +} +``` + +Or set them through the Web UI: open [http://localhost:3002](http://localhost:3002), go to the config panel, and edit the OpenBB section. Changes take effect immediately — no restart needed. + +The key names map to OpenBB credential fields automatically: +`fmp` → `fmp_api_key`, `eia` → `eia_api_key`, `fred` → `fred_api_key`, etc. + +## 6. What Can You Do? + +Once running, Alice has access to a rich set of market data tools powered by OpenTypeBB: + +### Market Search +Ask Alice to find any symbol across equities, crypto, and forex: +> "Search for Tesla stock" +> "Find crypto pairs with SOL" + +### Equity Data +- Price quotes and historical OHLCV +- Company profiles and financial statements +- Analyst estimates and earnings calendar +- Insider trading and institutional ownership +- Market movers (top gainers, losers, most active) + +### Crypto & Forex +- Real-time price data +- Historical OHLCV with configurable intervals + +### Technical Analysis +Built-in indicator calculator with formula expressions: +> "Calculate RSI(14) for AAPL on the daily chart" +> "Show me the 50-day and 200-day SMA crossover for BTC/USD" + +Uses syntax like `SMA(CLOSE('AAPL', '1d'), 50)`, `RSI(CLOSE('BTC/USD', '1d'), 14)`, etc. + +### Economy & Macro +- GDP data (OECD, IMF) +- FRED economic series (rates, inflation, employment) +- PCE, CPI, nonfarm payrolls, FOMC documents +- University of Michigan consumer sentiment +- Fed manufacturing outlook surveys + +### Commodities +- EIA petroleum & natural gas data +- Spot commodity prices + +### News +- Company-specific news +- World market news +- Background RSS collection with searchable archive + +## 7. Running OpenTypeBB as a Standalone HTTP Server + +If you want to use OpenTypeBB independently — for example, to connect it to [OpenBB Workspace](https://pro.openbb.co) or other tools — you can run it as a standalone API server: + +```bash +# From the repo root: +cd packages/opentypebb + +# Set your API key (optional — yfinance works without one) +export FMP_API_KEY=your_key_here + +# Run the server +npx tsx src/server.ts +``` + +The server starts on port 6901 (configurable via `OPENTYPEBB_PORT`): +``` +Built widgets.json with 88 widgets +OpenTypeBB listening on http://localhost:6901 +``` + +### API Endpoints + +The server exposes OpenBB-compatible REST endpoints: + +```bash +# Health check +curl http://localhost:6901/api/v1/health + +# Get a stock quote +curl "http://localhost:6901/api/v1/equity/price/quote?symbol=AAPL&provider=yfinance" + +# Get historical data +curl "http://localhost:6901/api/v1/equity/price/historical?symbol=MSFT&provider=yfinance&start_date=2024-01-01" + +# Get crypto price +curl "http://localhost:6901/api/v1/crypto/price/historical?symbol=BTC-USD&provider=yfinance" + +# Get world news (requires FMP key) +curl "http://localhost:6901/api/v1/news/world?provider=fmp&limit=5" + +# GDP data from OECD +curl "http://localhost:6901/api/v1/economy/gdp/nominal?provider=oecd&country=united_states" + +# Pass credentials per-request +curl -H 'X-OpenBB-Credentials: {"fmp_api_key": "your_key"}' \ + "http://localhost:6901/api/v1/equity/fundamental/income?symbol=AAPL&provider=fmp" + +# Discover available widgets (for OpenBB Workspace) +curl http://localhost:6901/widgets.json +``` + +### Embedded Server Mode + +You can also run the API server embedded inside OpenAlice (alongside the agent). Edit `data/config/openbb.json`: + +```json +{ + "apiServer": { + "enabled": true, + "port": 6901 + } +} +``` + +Then `pnpm dev` will start both Alice and the OpenTypeBB HTTP API. + +## 8. Using OpenTypeBB as a Library + +You can also import OpenTypeBB directly in your own TypeScript project: + +```typescript +import { createExecutor } from '@traderalice/opentypebb' + +const executor = createExecutor() + +// Get a stock quote (yfinance — no API key needed) +const quotes = await executor.execute('yfinance', 'EquityQuote', { + symbol: 'AAPL', +}, {}) + +console.log(quotes) + +// Get historical crypto data +const btcHistory = await executor.execute('yfinance', 'CryptoHistorical', { + symbol: 'BTC-USD', + start_date: '2024-01-01', +}, {}) + +// With an FMP API key +const income = await executor.execute('fmp', 'IncomeStatement', { + symbol: 'AAPL', + period: 'annual', +}, { + fmp_api_key: 'your_key_here', +}) +``` + +## 9. Architecture Overview + +``` +OpenAlice +├── packages/opentypebb/ # The OpenTypeBB library +│ ├── src/ +│ │ ├── index.ts # Library entry point +│ │ ├── server.ts # Standalone HTTP server +│ │ ├── core/ # Registry, executor, router, REST API +│ │ ├── providers/ # 14 data providers (yfinance, fmp, oecd, ...) +│ │ └── extensions/ # 9 domain routers (equity, crypto, economy, ...) +│ └── package.json +│ +├── src/openbb/ +│ ├── sdk/ # In-process SDK clients (equity, crypto, ...) +│ │ ├── executor.ts # Singleton QueryExecutor +│ │ ├── base-client.ts # Base class for SDK clients +│ │ └── *-client.ts # Domain-specific SDK clients +│ ├── equity/ # Equity data layer + SymbolIndex +│ ├── crypto/ # Crypto data layer +│ ├── currency/ # Currency/forex data layer +│ ├── commodity/ # Commodity data layer +│ ├── economy/ # Economy data layer +│ ├── news/ # News data layer +│ └── credential-map.ts # Config key → OpenBB credential mapping +│ +├── src/extension/ +│ ├── analysis-kit/ # Technical indicator calculator +│ ├── equity/ # Equity research tools +│ ├── market/ # Unified symbol search +│ └── news/ # News tools +│ +└── data/config/openbb.json # Runtime configuration +``` + +**Data flow (SDK mode):** +``` +Alice asks for AAPL quote + → ToolCenter dispatches to equity extension + → SDKEquityClient.getQuote() + → QueryExecutor.execute('yfinance', 'EquityQuote', { symbol: 'AAPL' }) + → YFinanceFetcher hits Yahoo Finance API + → Returns structured data +``` + +No HTTP. No Python. No sidecar. Just TypeScript all the way down. + +--- + +## TL;DR + +```bash +git clone https://github.com/TraderAlice/OpenAlice.git +cd OpenAlice +pnpm install && pnpm build +pnpm dev +# Open http://localhost:3002 and ask Alice about any stock, crypto, or macro data +``` + +Everything works out of the box. OpenTypeBB is the default data backend, `yfinance` is the default provider, and neither requires an API key. Add keys to `data/config/openbb.json` when you want to unlock more providers. diff --git a/package.json b/package.json index 2f96b61..df02e7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-alice", - "version": "0.9.0-beta.4", + "version": "0.9.0-beta.5", "description": "File-based trading agent engine", "type": "module", "scripts": { @@ -45,7 +45,7 @@ "express": "^5.2.1", "file-type": "^21.3.0", "grammy": "^1.40.0", - "hono": "^4.12.5", + "hono": "^4.12.7", "json5": "^2.2.3", "@traderalice/opentypebb": "workspace:*", "pino": "^10.3.1", diff --git a/packages/opentypebb/package.json b/packages/opentypebb/package.json index 298068f..1a95858 100644 --- a/packages/opentypebb/package.json +++ b/packages/opentypebb/package.json @@ -1,6 +1,6 @@ { "name": "@traderalice/opentypebb", - "version": "0.1.1", + "version": "0.1.2", "description": "TypeScript port of OpenBB Platform — financial data infrastructure", "type": "module", "exports": { diff --git a/packages/opentypebb/src/core/api/app-loader.ts b/packages/opentypebb/src/core/api/app-loader.ts index 6bcd3b4..fdb33c5 100644 --- a/packages/opentypebb/src/core/api/app-loader.ts +++ b/packages/opentypebb/src/core/api/app-loader.ts @@ -25,6 +25,7 @@ import { federalReserveProvider } from '../../providers/federal_reserve/index.js import { intrinioProvider } from '../../providers/intrinio/index.js' import { blsProvider } from '../../providers/bls/index.js' import { eiaProvider } from '../../providers/eia/index.js' +import { secProvider } from '../../providers/sec/index.js' import { stubProvider } from '../../providers/stub/index.js' // --- Extension routers --- @@ -57,6 +58,7 @@ export function createRegistry(): Registry { registry.includeProvider(intrinioProvider) registry.includeProvider(blsProvider) registry.includeProvider(eiaProvider) + registry.includeProvider(secProvider) registry.includeProvider(stubProvider) return registry } diff --git a/packages/opentypebb/src/index.ts b/packages/opentypebb/src/index.ts index 591a1aa..7dddce4 100644 --- a/packages/opentypebb/src/index.ts +++ b/packages/opentypebb/src/index.ts @@ -39,6 +39,10 @@ export { OpenBBError, EmptyDataError, UnauthorizedError } from './core/provider/ // App loader — convenience functions to create a fully-loaded system export { createRegistry, createExecutor, loadAllRouters } from './core/api/app-loader.js' +// Widget builder — for OpenBB Workspace frontend integration +export { buildWidgetsJson } from './core/api/widgets.js' +export { mountWidgetsEndpoint } from './core/api/rest-api.js' + // Pre-built providers (for direct import if needed) export { fmpProvider } from './providers/fmp/index.js' export { yfinanceProvider } from './providers/yfinance/index.js' diff --git a/packages/opentypebb/src/providers/sec/index.ts b/packages/opentypebb/src/providers/sec/index.ts new file mode 100644 index 0000000..941a228 --- /dev/null +++ b/packages/opentypebb/src/providers/sec/index.ts @@ -0,0 +1,18 @@ +/** + * SEC Provider. + * + * Source: https://www.sec.gov/ + * Free, no API key required. + */ + +import { Provider } from '../../core/provider/abstract/provider.js' +import { SECEquitySearchFetcher } from './models/equity-search.js' + +export const secProvider = new Provider({ + name: 'sec', + description: 'SEC EDGAR — US public company filings and data.', + website: 'https://www.sec.gov/', + fetcherDict: { + EquitySearch: SECEquitySearchFetcher, + }, +}) diff --git a/packages/opentypebb/src/providers/sec/models/equity-search.ts b/packages/opentypebb/src/providers/sec/models/equity-search.ts new file mode 100644 index 0000000..b9014a7 --- /dev/null +++ b/packages/opentypebb/src/providers/sec/models/equity-search.ts @@ -0,0 +1,89 @@ +/** + * SEC Equity Search Fetcher. + * + * Fetches the full company tickers list from SEC EDGAR (free, no API key). + * Source: https://www.sec.gov/files/company_tickers.json + * + * The JSON is a dict keyed by index: { "0": { cik_str, ticker, title }, ... } + * ~10,000 entries, sorted by market cap. + */ + +import { z } from 'zod' +import { Fetcher } from '../../../core/provider/abstract/fetcher.js' +import { amakeRequest } from '../../../core/provider/utils/helpers.js' +import { EquitySearchQueryParamsSchema, EquitySearchDataSchema } from '../../../standard-models/equity-search.js' + +// ==================== Provider-specific schemas ==================== + +export const SECEquitySearchQueryParamsSchema = EquitySearchQueryParamsSchema.extend({ + use_cache: z.boolean().default(true).describe('Whether to use the cache or not.'), + is_fund: z.boolean().default(false).describe('Whether to search the mutual funds/ETFs list.'), +}) + +export type SECEquitySearchQueryParams = z.infer + +export const SECEquitySearchDataSchema = EquitySearchDataSchema.extend({ + cik: z.string().describe('Central Index Key'), +}) + +export type SECEquitySearchData = z.infer + +// ==================== Raw SEC JSON shape ==================== + +interface SECTickerEntry { + cik_str: number + ticker: string + title: string +} + +// ==================== Fetcher ==================== + +const SEC_URL = 'https://www.sec.gov/files/company_tickers.json' +const SEC_HEADERS = { + 'User-Agent': 'OpenTypeBB/1.0 contact@example.com', + 'Accept-Encoding': 'gzip, deflate', +} + +export class SECEquitySearchFetcher extends Fetcher { + static override requireCredentials = false + + static override transformQuery(params: Record): SECEquitySearchQueryParams { + return SECEquitySearchQueryParamsSchema.parse(params) + } + + static override async extractData( + _query: SECEquitySearchQueryParams, + _credentials: Record | null, + ): Promise { + const raw = await amakeRequest>(SEC_URL, { + headers: SEC_HEADERS, + }) + + // raw is { "0": { cik_str, ticker, title }, "1": ... } + return Object.values(raw) + } + + static override transformData( + query: SECEquitySearchQueryParams, + data: SECTickerEntry[], + ): SECEquitySearchData[] { + const q = query.query.toLowerCase() + + // If empty query, return all (for bulk loading by SymbolIndex) + const filtered = q + ? data.filter((d) => + d.ticker.toLowerCase().includes(q) || + d.title.toLowerCase().includes(q) || + String(d.cik_str).includes(q), + ) + : data + + return filtered.map((d) => + SECEquitySearchDataSchema.parse({ + symbol: d.ticker, + name: d.title, + cik: String(d.cik_str), + }), + ) + } +} diff --git a/packages/opentypebb/src/standard-models/equity-search.ts b/packages/opentypebb/src/standard-models/equity-search.ts new file mode 100644 index 0000000..ef6d107 --- /dev/null +++ b/packages/opentypebb/src/standard-models/equity-search.ts @@ -0,0 +1,20 @@ +/** + * Equity Search Standard Model. + * Maps to: openbb_core/provider/standard_models/equity_search.py + */ + +import { z } from 'zod' + +export const EquitySearchQueryParamsSchema = z.object({ + query: z.string().default('').describe('Search query.'), + is_symbol: z.boolean().default(false).describe('Whether to search by ticker symbol.'), +}).passthrough() + +export type EquitySearchQueryParams = z.infer + +export const EquitySearchDataSchema = z.object({ + symbol: z.string().nullable().default(null).describe('Symbol of the company.'), + name: z.string().nullable().default(null).describe('Name of the company.'), +}).passthrough() + +export type EquitySearchData = z.infer diff --git a/packages/opentypebb/src/standard-models/index.ts b/packages/opentypebb/src/standard-models/index.ts index dab7f62..ad6453f 100644 --- a/packages/opentypebb/src/standard-models/index.ts +++ b/packages/opentypebb/src/standard-models/index.ts @@ -823,3 +823,12 @@ export { ChokepointVolumeDataSchema, type ChokepointVolumeData, } from './chokepoint-volume.js' + +// --- Equity Search --- + +export { + EquitySearchQueryParamsSchema, + type EquitySearchQueryParams, + EquitySearchDataSchema, + type EquitySearchData, +} from './equity-search.js' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4a33103..a12de96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 2.0.2(grammy@1.40.0) '@hono/node-server': specifier: ^1.19.11 - version: 1.19.11(hono@4.12.5) + version: 1.19.11(hono@4.12.7) '@modelcontextprotocol/sdk': specifier: ^1.26.0 version: 1.26.0(zod@4.3.6) @@ -63,8 +63,8 @@ importers: specifier: ^1.40.0 version: 1.40.0 hono: - specifier: ^4.12.5 - version: 4.12.5 + specifier: ^4.12.7 + version: 4.12.7 json5: specifier: ^2.2.3 version: 2.2.3 @@ -1803,10 +1803,6 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} - hono@4.12.5: - resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} - engines: {node: '>=16.9.0'} - hono@4.12.7: resolution: {integrity: sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==} engines: {node: '>=16.9.0'} @@ -3285,10 +3281,6 @@ snapshots: '@grammyjs/types@3.24.0': {} - '@hono/node-server@1.19.11(hono@4.12.5)': - dependencies: - hono: 4.12.5 - '@hono/node-server@1.19.11(hono@4.12.7)': dependencies: hono: 4.12.7 @@ -3422,7 +3414,7 @@ snapshots: '@modelcontextprotocol/sdk@1.26.0(zod@4.3.6)': dependencies: - '@hono/node-server': 1.19.11(hono@4.12.5) + '@hono/node-server': 1.19.11(hono@4.12.7) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -3432,7 +3424,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1 express-rate-limit: 8.2.1(express@5.2.1) - hono: 4.12.5 + hono: 4.12.7 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -4402,8 +4394,6 @@ snapshots: highlight.js@11.11.1: {} - hono@4.12.5: {} - hono@4.12.7: {} http-errors@2.0.1: diff --git a/src/connectors/web/routes/config.ts b/src/connectors/web/routes/config.ts index 6e3c55d..3353046 100644 --- a/src/connectors/web/routes/config.ts +++ b/src/connectors/web/routes/config.ts @@ -40,8 +40,8 @@ export function createConfigRoutes(opts?: ConfigRouteOpts) { } const body = await c.req.json() const validated = await writeConfigSection(section, body) - // Hot-reload connectors when their config changes - if (section === 'connectors') { + // Hot-reload connectors / OpenBB server when their config changes + if (section === 'connectors' || section === 'openbb') { await opts?.onConnectorsChange?.() } return c.json(validated) diff --git a/src/core/config.ts b/src/core/config.ts index a8605b7..66bbb37 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -124,9 +124,9 @@ const openbbSchema = z.object({ }).default({}), dataBackend: z.enum(['sdk', 'openbb']).default('sdk'), apiServer: z.object({ - enabled: z.boolean().default(false), + enabled: z.boolean().default(true), port: z.number().int().min(1024).max(65535).default(6901), - }).default({ enabled: false, port: 6901 }), + }).default({ enabled: true, port: 6901 }), }) const compactionSchema = z.object({ diff --git a/src/extension/trading/providers/alpaca/AlpacaAccount.ts b/src/extension/trading/providers/alpaca/AlpacaAccount.ts index 4bbc942..1e3f616 100644 --- a/src/extension/trading/providers/alpaca/AlpacaAccount.ts +++ b/src/extension/trading/providers/alpaca/AlpacaAccount.ts @@ -52,9 +52,16 @@ export class AlpacaAccount implements ITradingAccount { // ---- Lifecycle ---- private static readonly MAX_INIT_RETRIES = 5 + private static readonly MAX_AUTH_RETRIES = 2 private static readonly INIT_RETRY_BASE_MS = 1000 async init(): Promise { + if (!this.config.apiKey || !this.config.secretKey) { + throw new Error( + `No API credentials configured. Set apiKey and apiSecret in accounts.json to enable this account.`, + ) + } + this.client = new Alpaca({ keyId: this.config.apiKey, secretKey: this.config.secretKey, @@ -71,6 +78,13 @@ export class AlpacaAccount implements ITradingAccount { return } catch (err) { lastErr = err + const isAuthError = err instanceof Error && + /40[13]|forbidden|unauthorized/i.test(err.message) + if (isAuthError && attempt >= AlpacaAccount.MAX_AUTH_RETRIES) { + throw new Error( + `Authentication failed — verify your Alpaca API key and secret are correct.`, + ) + } if (attempt < AlpacaAccount.MAX_INIT_RETRIES) { const delay = AlpacaAccount.INIT_RETRY_BASE_MS * 2 ** (attempt - 1) console.warn(`AlpacaAccount[${this.id}]: init attempt ${attempt}/${AlpacaAccount.MAX_INIT_RETRIES} failed, retrying in ${delay}ms...`) diff --git a/src/extension/trading/providers/ccxt/CcxtAccount.ts b/src/extension/trading/providers/ccxt/CcxtAccount.ts index 6d2d48e..1d8ce96 100644 --- a/src/extension/trading/providers/ccxt/CcxtAccount.ts +++ b/src/extension/trading/providers/ccxt/CcxtAccount.ts @@ -105,6 +105,13 @@ export class CcxtAccount implements ITradingAccount { // ---- Lifecycle ---- async init(): Promise { + if (this.readOnly) { + console.log( + `CcxtAccount[${this.id}]: no API credentials — running in market-data-only mode. ` + + `Set apiKey and apiSecret in accounts.json for trading.`, + ) + } + // CCXT's fetchMarkets fires all market-type requests via Promise.all — // a single failure kills the entire batch. Monkey-patch fetchMarkets to // run each type sequentially with per-type retries. @@ -144,7 +151,14 @@ export class CcxtAccount implements ITradingAccount { } // Now loadMarkets will use our sequential fetchMarkets - await this.exchange.loadMarkets() + try { + await this.exchange.loadMarkets() + } catch (err) { + throw new Error( + `Failed to connect to ${this.exchangeName} — check network connectivity. ` + + `${err instanceof Error ? err.message : String(err)}`, + ) + } const marketCount = Object.keys(this.exchange.markets).length if (marketCount === 0) { diff --git a/src/main.ts b/src/main.ts index ceae7be..01297e2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,7 +31,7 @@ import { OpenBBEquityClient } from './openbb/equity/client.js' import { OpenBBCryptoClient } from './openbb/crypto/client.js' import { OpenBBCurrencyClient } from './openbb/currency/client.js' import { OpenBBNewsClient } from './openbb/news/client.js' -import { startEmbeddedOpenBBServer } from './server/opentypebb.js' +import { OpenBBServerPlugin } from './server/opentypebb.js' import { createMarketSearchTools } from './extension/market/index.js' import { createNewsTools } from './extension/news/index.js' import { createAnalysisTools } from './extension/analysis-kit/index.js' @@ -248,9 +248,7 @@ async function main() { newsClient = new SDKNewsClient(executor, 'news', undefined, credentials, routeMap) } - if (config.openbb.apiServer.enabled) { - startEmbeddedOpenBBServer(config.openbb.apiServer.port) - } + // OpenBB API server is started later via optionalPlugins // ==================== Equity Symbol Index ==================== @@ -443,6 +441,10 @@ async function main() { })) } + if (config.openbb.apiServer.enabled) { + optionalPlugins.set('openbb-server', new OpenBBServerPlugin({ port: config.openbb.apiServer.port })) + } + // ==================== Connector Reconnect ==================== let connectorsReconnecting = false @@ -484,6 +486,30 @@ async function main() { changes.push('telegram started') } + // --- OpenBB API Server --- + const openbbWanted = fresh.openbb.apiServer.enabled + const openbbRunning = optionalPlugins.has('openbb-server') + if (openbbRunning && !openbbWanted) { + await optionalPlugins.get('openbb-server')!.stop() + optionalPlugins.delete('openbb-server') + changes.push('openbb-server stopped') + } else if (!openbbRunning && openbbWanted) { + const p = new OpenBBServerPlugin({ port: fresh.openbb.apiServer.port }) + await p.start(ctx) + optionalPlugins.set('openbb-server', p) + changes.push('openbb-server started') + } else if (openbbRunning && openbbWanted) { + const current = optionalPlugins.get('openbb-server') as OpenBBServerPlugin + if (current.port !== fresh.openbb.apiServer.port) { + await current.stop() + optionalPlugins.delete('openbb-server') + const p = new OpenBBServerPlugin({ port: fresh.openbb.apiServer.port }) + await p.start(ctx) + optionalPlugins.set('openbb-server', p) + changes.push(`openbb-server restarted on port ${fresh.openbb.apiServer.port}`) + } + } + if (changes.length > 0) { console.log(`reconnect: connectors — ${changes.join(', ')}`) } @@ -533,6 +559,8 @@ async function main() { 'trading-ccxt', ) console.log('ccxt: provider tools registered') + }).catch((err) => { + console.error('ccxt: background init failed:', err instanceof Error ? err.message : String(err)) }) // ==================== Shutdown ==================== diff --git a/src/openbb/equity/SymbolIndex.ts b/src/openbb/equity/SymbolIndex.ts index a9d9a76..a27094f 100644 --- a/src/openbb/equity/SymbolIndex.ts +++ b/src/openbb/equity/SymbolIndex.ts @@ -36,7 +36,7 @@ interface CacheEnvelope { // ==================== Config ==================== /** 免费 provider 列表 — 扩展时在这里加 */ -const SOURCES = ['sec', 'tmx'] as const +const SOURCES = ['sec'] as const const CACHE_FILE = resolve('data/cache/equity/symbols.json') const CACHE_TTL_MS = 24 * 60 * 60 * 1000 // 24 hours diff --git a/src/server/opentypebb.ts b/src/server/opentypebb.ts index c4f2888..e6e0de7 100644 --- a/src/server/opentypebb.ts +++ b/src/server/opentypebb.ts @@ -9,9 +9,47 @@ import { Hono } from 'hono' import { cors } from 'hono/cors' import { serve } from '@hono/node-server' -import { createExecutor, loadAllRouters } from '@traderalice/opentypebb' +import { createExecutor, createRegistry, loadAllRouters, buildWidgetsJson, mountWidgetsEndpoint } from '@traderalice/opentypebb' +import type { Plugin, EngineContext } from '../core/types.js' + +export class OpenBBServerPlugin implements Plugin { + readonly name = 'openbb-server' + readonly port: number + private server: ReturnType | null = null + + constructor(opts: { port: number }) { + this.port = opts.port + } + + async start(_ctx: EngineContext): Promise { + const registry = createRegistry() + const executor = createExecutor() + + const app = new Hono() + app.use(cors()) + app.get('/api/v1/health', (c) => c.json({ status: 'ok' })) + + const rootRouter = loadAllRouters() + + const widgetsJson = buildWidgetsJson(rootRouter, registry) + mountWidgetsEndpoint(app, widgetsJson) + console.log(`[openbb] Built widgets.json with ${Object.keys(widgetsJson).length} widgets`) + + rootRouter.mountToHono(app, executor) + + this.server = serve({ fetch: app.fetch, port: this.port }) + console.log(`[openbb] Embedded API server listening on http://localhost:${this.port}`) + } + + async stop(): Promise { + this.server?.close() + this.server = null + console.log('[openbb] Embedded API server stopped') + } +} export function startEmbeddedOpenBBServer(port: number): void { + const registry = createRegistry() const executor = createExecutor() const app = new Hono() @@ -19,6 +57,12 @@ export function startEmbeddedOpenBBServer(port: number): void { app.get('/api/v1/health', (c) => c.json({ status: 'ok' })) const rootRouter = loadAllRouters() + + // Build and mount widgets.json for OpenBB Workspace frontend + const widgetsJson = buildWidgetsJson(rootRouter, registry) + mountWidgetsEndpoint(app, widgetsJson) + console.log(`[openbb] Built widgets.json with ${Object.keys(widgetsJson).length} widgets`) + rootRouter.mountToHono(app, executor) serve({ fetch: app.fetch, port })